Flutter 记GetX依赖绑错路由的问题
前景
前端组在21年中旬经过fish_redux烂尾行为决定放弃使用,转为投入GetX的怀抱.GetX的便利这里就不在赘述,下面分享在实际项目中遇到的一些问题及解决办法
问题起源
-
本来某天高高兴兴上班中,测试过来说首页发现一个不稳定复现bug,别慌,一般不稳定复现的都跟设备呀网络呀有关,这种问题叫网管重启一下就好了[\doge].一会后测试再报,不行,并且强烈要求尽快解决
-
查看日志是GetX抛出的异常,类似于
,em~~~,这,控制器找不到应该是问题bug,忘了注册或者不小心在哪里删除了都是有可能的,但是这种低级错误应该不会由测试发现才对,也不应该是偶现的
-
复现流程,然后检查Getx日志,在退弹窗路由时错误的将首页的组件A的logic给一起删掉,所以首页想继续用的时候直接抛出了异常
-
查阅文档 Getx依赖管理-Bindings,对比项目结构发现使用没问题,bindings:
route绑定:
问题是出在首页组件这里,它需要依赖后端api展示,后端返回数据才初始化组件A的logic(依赖懒加载),这里弹窗已经打开了,从结果上来看,logic依赖了错误的路由.导致回到首页已经被销毁所以报错.那么只要弄清楚logic绑错路由的原因就可以解决了
查看源码
- 首先,懒加载注册依赖的时机是view里find对应的logic时Get.find() 源码,其中
final i = _initDependencies<S>(name: tag, route: route);
return i ?? dep.getDependency() as S;
这两行执行加载依赖并绑定路由
/// Initializes the dependencies for a Class Instance [S] (or tag),
/// If its a Controller, it starts the lifecycle process.
/// Optionally associating the current Route to the lifetime of the instance,
/// if `Get.smartManagement` is marked as [SmartManagement.full] or
/// [SmartManagement.keepFactory]
/// Only flags `isInit` if it's using `Get.create()`
/// (not for Singletons access).
/// Returns the instance if not initialized, required for Get.create() to
/// work properly.
S? _initDependencies<S>({String? name}) {
final key = _getKey(S, name);
final isInit = _singl[key]!.isInit;
S? i;
if (!isInit) {
i = _startController<S>(tag: name);
if (_singl[key]!.isSingleton!) {
_singl[key]!.isInit = true;
if (Get.smartManagement != SmartManagement.onlyBuilder) {
///将依赖绑定到路由
RouterReportManager.instance
.reportDependencyLinkedToRoute(_getKey(S, name));
}
}
}
return i;
}
reportDependencyLinkedToRoute: 这个方法将depedency直接添加到对应routeKey的一个map里,在退路由时,将对应key里的depedency置为dirty,然后等待移除,具体实现见同文件
/// Links a Class instance [S] (or [tag]) to the current route.
/// Requires usage of `GetMaterialApp`.
void reportDependencyLinkedToRoute(String depedencyKey) {
if (_current == null) return;
if (_routesKey.containsKey(_current)) {
_routesKey[_current!]!.add(depedencyKey);
} else {
_routesKey[_current] = <String>[depedencyKey];
}
}
其中_current 便是当前路由.到这里问题就明确了,因为组件A执行find()时路由已经是弹窗了,所以绑定的routeKey的,那么退弹窗时logic close了,然后首页使用结果找不到
解决思路
-
按表现来说,既然是依赖懒加载导致的,那么不使用
Get.lazyPut直接使用Get.put,或者在首页build方法里Get.find一下对应的logic应该就能解决,但是业务上不可以,因为组件A因为某些原因必须懒加载,故此方法放弃 -
从问题产生来讲,错误发生在依赖绑定路由的时候,因为
_current路由不一定是这个依赖对应的路由,那么在特殊地方直接将对应的路由传进去替代使用_current,这个问题就可以解决了:- 首先view.find logic时要拿到对应的路由,那么view所对应的路由应该怎么取到呢,没错,就是
BuildContext(注意,这不能使用Get.context),获取路由ModalRoute.of(context)返回一个Route对象,然后将Route一层一层传给绑定方法
find:
/// Finds the registered type <[S]> (or [tag]) /// In case of using Get.[create] to register a type <[S]> or [tag], /// it will create an instance each time you call [find]. /// If the registered type <[S]> (or [tag]) is a Controller, /// it will initialize it's lifecycle. S find<S>({String? tag, Route? route}) { final key = _getKey(S, tag); if (isRegistered<S>(tag: tag)) { final dep = _singl[key]; if (dep == null) { if (tag == null) { throw 'Class "$S" is not registered'; } else { throw 'Class "$S" with tag "$tag" is not registered'; } } // if (dep.lateRemove != null) { // dep.isDirty = true; // if(dep.fenix) // } /// although dirty solution, the lifecycle starts inside /// `initDependencies`, so we have to return the instance from there /// to make it compatible with `Get.create()`. final i = _initDependencies<S>(name: tag, route: route); return i ?? dep.getDependency() as S; } else { // ignore: lines_longer_than_80_chars throw '"$S" not found. You need to call "Get.put($S())" or "Get.lazyPut(()=>$S())"'; } }_initDependencies:
/// Initializes the dependencies for a Class Instance [S] (or tag), /// If its a Controller, it starts the lifecycle process. /// Optionally associating the current Route to the lifetime of the instance, /// if `Get.smartManagement` is marked as [SmartManagement.full] or /// [SmartManagement.keepFactory] /// Only flags `isInit` if it's using `Get.create()` /// (not for Singletons access). /// Returns the instance if not initialized, required for Get.create() to /// work properly. S? _initDependencies<S>({String? name, Route? route}) { final key = _getKey(S, name); final isInit = _singl[key]!.isInit; S? i; if (!isInit) { i = _startController<S>(tag: name, route: route); if (_singl[key]!.isSingleton!) { _singl[key]!.isInit = true; if (Get.smartManagement != SmartManagement.onlyBuilder) { RouterReportManager.reportDependencyLinkedToRoute(_getKey(S, name), route: route); } } } return i; }reportDependencyLinkedToRoute:
/// Links a Class instance [S] (or [tag]) to the current route. /// Requires usage of `GetMaterialApp`. static void reportDependencyLinkedToRoute(String depedencyKey, {Route? route}) { if (route == null && _current == null) return; if (_routesKey.containsKey(route ?? _current)) { _routesKey[route ?? _current!]!.add(depedencyKey); } else { _routesKey[route ?? _current] = <String>[depedencyKey]; } } - 首先view.find logic时要拿到对应的路由,那么view所对应的路由应该怎么取到呢,没错,就是
这样将特殊情况特殊处理之后也能达成效果,但是改动源码之后的使用又变得复杂了.
后记
总体来说这问题比较容易发现,但是用了比较笨的方法解决,暂时没有别的好的思路,我也在github上提了issues,说了下我的想法,但是好像没有人理我,可能他们没有这种业务需求吧
广告
惠群2022前端招新啦,前端组19年开始使用flutter,并一直坚持flutter打造公司通用技术栈的道路,经过几年的沉淀,我们在app端,桌面端,web端都有项目产出,并有着自己的规范及插件库,我们欢迎志同道合且喜欢flutter的同学加入公司邮箱:wlm@wiqun.com