记GetX依赖绑错路由的问题

538 阅读4分钟

Flutter 记GetX依赖绑错路由的问题

前景

前端组在21年中旬经过fish_redux烂尾行为决定放弃使用,转为投入GetX的怀抱.GetX的便利这里就不在赘述,下面分享在实际项目中遇到的一些问题及解决办法

问题起源

  1. 本来某天高高兴兴上班中,测试过来说首页发现一个不稳定复现bug,别慌,一般不稳定复现的都跟设备呀网络呀有关,这种问题叫网管重启一下就好了[\doge].一会后测试再报,不行,并且强烈要求尽快解决

  2. 查看日志是GetX抛出的异常,类似于 alt 抛出的错误,em~~~,这,控制器找不到应该是问题bug,忘了注册或者不小心在哪里删除了都是有可能的,但是这种低级错误应该不会由测试发现才对,也不应该是偶现的

  3. 复现流程,然后检查Getx日志,在退弹窗路由时错误的将首页的组件A的logic给一起删掉,所以首页想继续用的时候直接抛出了异常
    模拟的流程

  4. 查阅文档 Getx依赖管理-Bindings,对比项目结构发现使用没问题,bindings: Bindings route绑定: 路由绑定 问题是出在首页组件这里,它需要依赖后端api展示,后端返回数据才初始化组件A的logic(依赖懒加载),这里弹窗已经打开了,从结果上来看,logic依赖了错误的路由.导致回到首页已经被销毁所以报错.那么只要弄清楚logic绑错路由的原因就可以解决了

查看源码

  1. 首先,懒加载注册依赖的时机是view里find对应的logic时Get.find() 源码,其中
final i = _initDependencies<S>(name: tag, route: route); 
return i ?? dep.getDependency() as S;

这两行执行加载依赖并绑定路由

  1. _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}) {
 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;
}
  1. 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,这个问题就可以解决了:

    1. 首先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())"';
    }
    }
    
    1. _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;
    }
    
    1. 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];
    }
    }
    

这样将特殊情况特殊处理之后也能达成效果,但是改动源码之后的使用又变得复杂了.

后记

总体来说这问题比较容易发现,但是用了比较笨的方法解决,暂时没有别的好的思路,我也在github上提了issues,说了下我的想法,但是好像没有人理我,可能他们没有这种业务需求吧

广告

惠群2022前端招新啦,前端组19年开始使用flutter,并一直坚持flutter打造公司通用技术栈的道路,经过几年的沉淀,我们在app端,桌面端,web端都有项目产出,并有着自己的规范及插件库,我们欢迎志同道合且喜欢flutter的同学加入公司邮箱:wlm@wiqun.com