在Flutter众多关于状态管理的开源框架中,GetX 无疑是非常闪亮的那个“星”,其功能非常丰富、强大,这是GetX 的最大的优点,也是它隐形的缺点,即如果不对GetX 的源码(原理)有较为深入的理解,很容易会在使用时出现一些错误。
我们都知道,在使用GetX 的时候,通常会将业务处理放在Controller中,然后在View中初始化Controller,进而将Controller和View进行绑定。例如:
class Lala extends StatelessWidget{
final logic = Get.put(LalaController());
@override
Widget build(BuildContext context) {
return GetBuilder<LalaController>(builder: (logic) {
return Text(
'点击了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0),
);
});
}
}
class LalaController extends GetxController {
int counter = 0;
var count = 0.obs;
void increment() {
counter++;
update(); // 当调用增量时,使用update()来更新用户界面上的计数器变量。
}
@override
void onInit() {
print("$this onReady");
super.onInit();
}
@override
void onReady() {
print("$this onReady");
super.onReady();
}
@override
void onClose() {
print("$this onClose");
super.onClose();
}
}
上面这段代码是非常简单的GetX 使用demo,很多博客甚至官方也是这么写的。
但是,当你运行这段代码时,你会发现一个问题,就是Controller 里的声明周期方法中的日志仅输出了一次。
@override
void onInit() {
print("$this onReady");
super.onInit();
}
@override
void onReady() {
print("$this onReady");
super.onReady();
}
@override
void onClose() {
print("$this onClose");
super.onClose();
}
除非你把APP杀了重新进入,上面的生命周期日志才会重新输出,否则当前页面无论怎么进入、退出,生命周期方法都不会重新执行!!!
这就非常麻烦了,如果Controller 生命周期不与所属View进行绑定,会非常容易造成内存泄漏的问题,因为有些资源释放的逻辑一旦放在上面的onClose方法中,是一定执行不到的。
关于这个问题,网上也有一些解释,甚至是解决方案,记得有的博主说:
“若想让Controller 能够感知到View 的生命周期,必须使用GetX自带的路由才行,或者是在View的dispose 时,手动调用delete<自己的Controller>”
在我看来上述的描述是有问题的:
接下来我们就从源码中好好分析下问题的原因。官方源码中,在View的dispose 时是会执行delete<自己的Controller>的,但是!有限制条件,为此我后来单步调试才找到问题的根源。下面看下关键源码:
get_state.dart
@override
void dispose() {
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。理由如下:
const GetBuilder({
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。
@override
void initState() {
// _GetBuilderState._currentState = this;
super.initState();
widget.initState?.call(this);
/// 1
var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);
if (widget.global) {
/// 2
if (isRegistered) {
/// 3
if (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) {
return false;
}
if (!builder.isInit) {
return true;
}
return false;
}
所以代码最终会执行到4,也就是讲_isCreator变成true,从_isCreator 单词的字面意义上看_isCreator 在这个阶段的值是true没有问题。但是回到我们前面的判断逻辑:
(_isCreator! || widget.assignId)
_isCreator! 就会有问题,这会导致这层判断条件永远无法正常true,进而导致Controller的生命周期无法和View关联。
通过前面分析,我们可以知道导致Controller的生命周期无法和View关联就是:
(_isCreator! || widget.assignId)
又知道_isCreator 的值在后面的场合又有多次改动,因此修改这个显然有些风险,因此只需要修改widget.assignId,让其值为true即可解决问题。
下面是修改后的代码:
class Lala extends StatelessWidget{
@override
Widget build(BuildContext context) {
return GetBuilder<LalaController>(builder: (logic) {
return Text(
'点击了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0),
);
},assignId: true,);
}
}
上面就是在GetBuilder 初始化时手动修改assignId: true,即可解决Controller生命周期调用异常的问题。
解决问题的办法有很多,一定要选择一个简单优雅的办法。