一、前言
在Flutter众多关于状态管理的开源框架中,GetX 无疑是非常闪亮的那个“星”,其功能非常丰富、强大,这是GetX 的最大的优点,也是它隐形的缺点,即如果不对GetX 的源码(原理)有较为深入的理解,很容易会在使用时出现一些错误。
二、问题
我们都知道,在使用GetX 的时候,通常会将业务处理放在Controller中,然后在View中初始化Controller,进而将Controller和View进行绑定。例如:
classLalaextendsStatelessWidget{final logic =Get.put(LalaController());@overrideWidgetbuild(BuildContext context){returnGetBuilder<LalaController>(builder:(logic){returnText('点击了 ${logic.count} 次',
style:TextStyle(fontSize:30.0),);});}}
classLalaControllerextendsGetxController{
int counter =0;var count =0.obs;voidincrement(){
counter++;update();// 当调用增量时,使用update()来更新用户界面上的计数器变量。}@overridevoidonInit(){print("$this onReady");super.onInit();}@overridevoidonReady(){print("$this onReady");super.onReady();}@overridevoidonClose(){print("$this onClose");super.onClose();}}
上面这段代码是非常简单的GetX 使用demo,很多博客甚至官方也是这么写的。
但是,当你运行这段代码时,你会发现一个问题,就是Controller 里的声明周期方法中的日志仅输出了一次。
@overridevoidonInit(){print("$this onReady");super.onInit();}@overridevoidonReady(){print("$this onReady");super.onReady();}@overridevoidonClose(){print("$this onClose");super.onClose();}
除非你把APP杀了重新进入,上面的生命周期日志才会重新输出,否则当前页面无论怎么进入、退出,生命周期方法都不会重新执行!!!
这就非常麻烦了,如果Controller 生命周期不与所属View进行绑定,会非常容易造成内存泄漏的问题,因为有些资源释放的逻辑一旦放在上面的onClose方法中,是一定执行不到的。
三、问题分析
关于这个问题,网上也有一些解释,甚至是解决方案,记得有的博主说:
“若想让Controller 能够感知到View 的生命周期,必须使用GetX自带的路由才行,或者是在View的dispose 时,手动调用delete<自己的Controller>”
在我看来上述的描述是有问题的:
- 首先,GetX 里并没有强行推他的路由功能,如果将Controller的生命周期和路由强制绑定,这样的设计实在太烂了!因为这是两个完全挨不上的功能。
- 其次,关于路由功能,各大厂都有自己的路由定制,原生的路由也够用,使用GetX 的路由未必就是最好的方案。以Android为例,几乎每个大厂都有自己的路由框架,比如ARouter、DRouter…,我当前所在的公司就用的自己研发的路由框架,就连Flutter 我们自己也搞了一个路由工具库。
- 最后,关于在View的dispose 时,手动调用delete<自己的Controller>的实现未免也太麻烦了,一点也不优雅。当然,如果你不嫌麻烦,也可以这么写。
接下来我们就从源码中好好分析下问题的原因。官方源码中,在View的dispose 时是会执行delete<自己的Controller>的,但是!有限制条件,为此我后来单步调试才找到问题的根源。下面看下关键源码:
get_state.dart
@overridevoiddispose(){super.dispose();
widget.dispose?.call(this);if(_isCreator!|| widget.assignId){if(widget.autoRemove &&GetInstance().isRegistered<T>(tag: widget.tag)){GetInstance().delete<T>(tag: widget.tag);}}....}
下面我们就分析这段代码,核心代码:
GetInstance().delete<T>(tag: widget.tag);
在执行这段之前,有两层判断。分别是:
/// 第一层if(_isCreator!|| widget.assignId)/// 第二层if(widget.autoRemove &&GetInstance().isRegistered<T>(tag: widget.tag))
先来看第二层,这里可以先给出结论,第二层判断一定为true。理由如下:
constGetBuilder({Key? key,this.init,this.global =true,
required this.builder,this.autoRemove =true,///this.assignId =false,/// 注意.....
在GetBuilder 构造方法中autoRemove默认就是true,除非你手动改。而GetInstance().isRegistered(tag: widget.tag)的意思是你自定义的Controller是否注册了,这个也一定是的,如果你在init阶段不注册,后面你都无法正常使用。所以,第二层判断一定为true。
接下来就来看看第一层判断:
(_isCreator!|| widget.assignId)
这里是个或逻辑判断,也就是说上面两个值只要有一个true,就可以让最后的关键代码执行。从当前现象上来看,这两个结果都是false。
- 首先 widget.assignId是false的原因是在GetBuilder 构造方法中默认就是 this.assignId = false,这个值后面没有地方修改。
- 其次就是_isCreator!,这个值非常的坑,背后牵扯一大堆逻辑,既然_isCreator! 是false,说明_isCreator 的值是true,表示是否已经创建。
@overridevoidinitState(){// _GetBuilderState._currentState = this;super.initState();
widget.initState?.call(this);/// 1var isRegistered =GetInstance().isRegistered<T>(tag: widget.tag);if(widget.global){/// 2if(isRegistered){/// 3if(GetInstance().isPrepared<T>(tag: widget.tag)){/// 4
_isCreator =true;}else{
_isCreator =false;}
controller =GetInstance().find<T>(tag: widget.tag);}else{
controller = widget.init;
_isCreator =true;GetInstance().put<T>(controller!, tag: widget.tag);}}else{
controller = widget.init;
_isCreator =true;
controller?.onStart();}if(widget.filter !=null){
_filter = widget.filter!(controller!);}_subscribeToController();}
上面的代码就是给_isCreator 赋值的地方,首先1、2一定都是true,因为在View执行initState之前,Controller一定执行注册好了(大家感兴趣的话可以自行调试看看),至于3,也是true.
bool isPrepared<S>({String? tag}){final newKey =_getKey(S, tag);final builder = _getDependency<S>(tag: tag, key: newKey);if(builder ==null){returnfalse;}if(!builder.isInit){returntrue;}returnfalse;}
所以代码最终会执行到4,也就是讲_isCreator变成true,从_isCreator 单词的字面意义上看_isCreator 在这个阶段的值是true没有问题。但是回到我们前面的判断逻辑:
(_isCreator!|| widget.assignId)
_isCreator! 就会有问题,这会导致这层判断条件永远无法正常true,进而导致Controller的生命周期无法和View关联。
四、问题解决
通过前面分析,我们可以知道导致Controller的生命周期无法和View关联就是:
(_isCreator!|| widget.assignId)
又知道_isCreator 的值在后面的场合又有多次改动,因此修改这个显然有些风险,因此只需要修改widget.assignId,让其值为true即可解决问题。
下面是修改后的代码:
classLalaextendsStatelessWidget{@overrideWidgetbuild(BuildContext context){returnGetBuilder<LalaController>(builder:(logic){returnText('点击了 ${logic.count} 次',
style:TextStyle(fontSize:30.0),);},assignId:true,);}}
上面就是在GetBuilder 初始化时手动修改assignId: true,即可解决Controller生命周期调用异常的问题。
五、总结
解决问题的办法有很多,一定要选择一个简单优雅的办法。
版权归原作者 咖啡老师 所有, 如有侵权,请联系我们删除。