当前位置: 首页 > 工具软件 > GetX > 使用案例 >

解决GetX Controller 生命周期方法调用异常的BUG

陶俊晤
2023-12-01

一、前言

在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>”

在我看来上述的描述是有问题的:

  • 首先,GetX 里并没有强行推他的路由功能,如果将Controller的生命周期和路由强制绑定,这样的设计实在太烂了!因为这是两个完全挨不上的功能。
  • 其次,关于路由功能,各大厂都有自己的路由定制,原生的路由也够用,使用GetX 的路由未必就是最好的方案。以Android为例,几乎每个大厂都有自己的路由框架,比如ARouter、DRouter…,我当前所在的公司就用的自己研发的路由框架,就连Flutter 我们自己也搞了一个路由工具库。
  • 最后,关于在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。

  • 首先 widget.assignId是false的原因是在GetBuilder 构造方法中默认就是 this.assignId = false,这个值后面没有地方修改。
  • 其次就是_isCreator!,这个值非常的坑,背后牵扯一大堆逻辑,既然_isCreator! 是false,说明_isCreator 的值是true,表示是否已经创建。
@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生命周期调用异常的问题。

五、总结

解决问题的办法有很多,一定要选择一个简单优雅的办法。

 类似资料: