深入剖析Flutter Getx框架状态管理机制

2,319 阅读4分钟

楔子

最近写Flutter项目的时候接触了getx框架,深深被这个它所吸引,功能强大,api简洁,状态管理机制也堪称优秀,本篇就简单分析一下getx是如何通过Obx和obs就能实现状态管理的。

文章很大,你忍一下!

计数器Demo

首先,我们还是以一个计数器的demo作为引子,使用getx实现计数器功能如下:

void main() {
  runApp(MaterialApp(
    home: CounterDemoPage(),
  ));
}

class CounterDemoPage extends StatelessWidget {
  var counter = 0.obs;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("计数器Demo"),
      ),
      body: Center(
        child: Obx(() => Text("计数结果 = ${counter.value}")),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.value += 1;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

在getx框架的支持下,CounterDemoPage不再需要继承StatefulWidget和使用setState()方法而是通过以上代码就能完成和官方提供Demo一样的功能,不得不让人赞叹一句:有点东西!

那getx是如何通过如此简单的操作就完成了状态管理,接下来我们深入的剖析一下。

拓展函数obs

首先跟踪代码obs,发现这是一个对int类型的拓展函数,在调用obs时会返回一个RxInt的对象(虽然是Rx开头的,但并不是RxDart框架)。同时getx对其他的一些基本数据类型和对象类型都有所拓展,具体请查看getx的官方文档。

extension IntExtension on int {
  RxInt get obs => RxInt(this);
}

class RxInt extends Rx<int> {
  RxInt(int initial) : super(initial);

  /// Addition operator.
  RxInt operator +(int other) {
    value = value + other;
    return this;
  }

  /// Subtraction operator.
  RxInt operator -(int other) {
    value = value - other;
    return this;
  }
}

RxInt类里面除了重写了+和-的运算符外,好像并没有什么值得关注的了,那就继续追踪父类。

class Rx<T> extends _RxImpl<T> {
  Rx(T initial) : super(initial);

  @override
  dynamic toJson() {
    /// 略去无关代码
  }
}

Rx 里面只实现了一个toJson的方法,继续往上追溯 _RxImpl

abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {
	 _RxImpl(T initial) {
    _value = initial;
  }

  void addError(Object error, [StackTrace? stackTrace]) {
     ///略...
  }

  Stream<R> map<R>(R mapper(T? data)) => stream.map(mapper);
  
  void update(void fn(T? val)) {
     ///略...
  }
  
  void trigger(T v) {
    ///略...
  }
}

_RxImpl中实现了四个方法,addError是发送错误通知,updatetrigger是更新数据相关方法,map是对Stream类中map方法的封装,貌似都不是我们要找的。不过该类继承了RxNotifier(重要) 并混入了RxObjectMixin,由于RxNotifier过于重要,我们先看混入的RxObjectMixin

mixin RxObjectMixin<T> on NotifyManager<T> {
  late T _value;

  /// 刷新数据
  void refresh() {
    subject.add(value);
  }

  ///调用set value并返回get value值
  T call([T? v]) {
    if (v != null) {
      value = v;
    }
    return value;
  }

  /// 是否第一次创建
  bool firstRebuild = true;

  ///...略去无关代码
  
  /// 更新数据
  set value(T val) {
    /// 略...
  }

  /// 获取_value的值
  T get value {
    /// #预留问题1 这个地方是个重点!这个地方是个重点!!这个地方是个重点!!!重要的事情说三遍
    RxInterface.proxy?.addListener(subject);
    return _value;
  }

  /// 通过subject拿到strema
  Stream<T?> get stream => subject.stream;

  ///下面两行为getx类库注释的翻译,我并没有找到这个方法调用的位置,应该是留给开发者调用的。
  /// 将现有的Stream<T>绑定到此 Rx<T> 以保持值同步。 (注释翻译)
  /// 您可以绑定多个源来更新值。 当观察者小部件( GetX或Obx )从小部件树中卸载时,将自动关闭订阅(注释翻译)
  void bindStream(Stream<T> stream) {
    final listSubscriptions =
        _subscriptions[subject] ??= <StreamSubscription>[];
    listSubscriptions.add(stream.listen((va) => value = va));
  }
}

可以看出RxObjectMixin中基本上都是关于更新数据的方法,但是数据更新后怎么通知到视图,这里也并没有提现出来,但是这个类实现了NotifyManager类,我们先不看NotifyManager而是回到 _RxImpl类,看它继承的父类。

class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;

_RxImpl的父类RxNotifier继承自RxInterface并和RxObjectMixin一样混入和NotifyManager

先来看一下RxInterface:

abstract class RxInterface<T> {
  ///看名字也知道,是个判断是否可以更新的方法
  bool get canUpdate;

  void addListener(GetStream<T> rxGetx);

  void close();

  static RxInterface? proxy;

  StreamSubscription<T> listen(void Function(T event) onData,
      {Function? onError, void Function()? onDone, bool? cancelOnError});

  /// Avoids an unsafe usage of the `proxy`
  static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    /// #预留问题2 下面会介绍到该方法
    
  }
}

RxInterface中有四个未实现的方法,分别是get方法canUpdateaddListenercloselisten,但是都没有具体实现。而静态方法notifyChildren在前面的追踪中也没有发现在哪个地方有调用,所以暂时不用管它,静态变量proxy倒是在上面标记重点的地方出现了,不过既然是重点,要肯定要留到最后再说。

RxNotifier继承了RxInterface同时也继承了四个为实现的方法,这四个方法在混入的NotifyManager中得到了实现:

mixin NotifyManager<T> {
  GetStream<T> subject = GetStream<T>();
  final _subscriptions = <GetStream, List<StreamSubscription>>{};

  ///根据_subscriptions是否为空判断是否可以更新
  bool get canUpdate => _subscriptions.isNotEmpty;
  
  /// #预留问题3 这个放到后面介绍
  void addListener(GetStream<T> rxGetx) {
    if (!_subscriptions.containsKey(rxGetx)) {
      final subs = rxGetx.listen((data) {
        if (!subject.isClosed) subject.add(data);
      });
      final listSubscriptions =
          _subscriptions[rxGetx] ??= <StreamSubscription>[];
      listSubscriptions.add(subs);
    }
  }

  
  /// #预留问题4 看起来像是监听数据的更新,但是在前面的分析中,并没有发现调用的地方,那我们就先放着吧
  StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,
      );

  /// 对外提供close方法清理内存
  void close() {
    /// 略...
  }
}

NotifyManager是obs继承链条可以追踪到最终的类了,它里面持有了一个GetStream类型的属性subjectGetStream也是getx框架状态管理的核心类,subjectaddListenerlisten中都有用到,不过我们暂时先缓一缓,看一下Obx的实现。

自定义Widget Obx

分析完obs的实现,然我们看一下自定义Widget Obx是如何实现的:

typedef WidgetCallback = Widget Function();

class Obx extends ObxWidget {
  final WidgetCallback builder;

  const Obx(this.builder);

  @override
  Widget build() => builder();
}

Obx类继承了ObxWidget,但类的功能非常简单,只是在构造函数中声明了一个必传的参数,而参数的类型则是一个返回值为Widget的无参函数,然后在重写父类的build方法中直接调用该函数。

看来核心的东西应该都在Obx的父类ObxWidget中了,我们继续深入。

abstract class ObxWidget extends StatefulWidget {
  //... 略去无用代码
  @override
  _ObxState createState() => _ObxState();

  ///这个build方法时getx框架定义的,而非系统方法
  @protected
  Widget build();
}

ObxWidget是一个继承自StatefulWidget的Widget,总所周知,StatefulWidget是需要有一个对应的State来管理Widget,然后查看 _ObxState的具体实现。

class _ObxState extends State<ObxWidget> {
  
  #2 /// 又见到了这个类
  final _observer = RxNotifier();
  
  #1
  late StreamSubscription subs;

  #3
  @override
  void initState() {
    super.initState();
    ///这里调用了RxNotifier的listen方法,也就是NotifyManager的listen方法,这是 #预留问题4 中方法调用的地方
    subs = _observer.listen(_updateTree, cancelOnError: false);
  }

  #4
  void _updateTree(_) {
    if (mounted) {
      setState(() {});
    }
  }

  #5
  @override
  void dispose() {
    subs.cancel();
    _observer.close();
    super.dispose();
  }

  #6
  @override
  Widget build(BuildContext context) =>
      RxInterface.notifyChildren(_observer, widget.build);
}

在这里可以看到 _ObxState持有两个属性。

#1 StreamSubscription是Dart的Steam中提供的类想了解Steam的请查阅Dart | 什么是Stream,定义了订阅事件的各种方法以及取消方法。在当前类中,StreamSubscription的作用就是在 #5 dispose生命周期中取消订阅,防止内存泄漏的发生。

#2 我们在追踪obs的实现的时候,标记了一个非常重要的类RxNotifier,在这里又再次遇见了它,我们创建的每一个Obx对象内部都持有一个RxNotifier的实现。

_ObxState#3 initState生命周期中, _observer调用listen方法开始监听数据,并把 _updateTree() 函数作为参数传了过去,当数据更新时, #3 _updateTree() 会被调用,而它又调用了setState方法,当setState方法被调用的时候, #6 build方法就会被调用,重新返回一个数据更新后的Widget,从而实现了界面的刷新。

我们继续追踪 #3listen方法,最后在get_stream.dart类中,每个listen方法都会创建一个LightSubscription对象,缓存到了属性List<LightSubscription>? _onData中,等待着被临幸,而而LightSubscription正是 #1 StreamSubscription的子类,被 _ObxState中的subs接收。

mixin NotifyManager<T> {
   GetStream<T> subject = GetStream<T>();
  //NotifyManager中的listen方法
   StreamSubscription<T> listen(
      void Function(T) onData, {
      Function? onError,
      void Function()? onDone,
      bool? cancelOnError,
    }) =>
        //直接调用了GetStream对象中的listen方法
        subject.listen(
          onData,
          onError: onError,
          onDone: onDone,
          cancelOnError: cancelOnError ?? false,
        );
 }
  
class GetStream<T> {
   List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];
  
   // GetStream中的listen方法
   LightSubscription<T> listen(void Function(T event) onData,
        {Function? onError, void Function()? onDone, bool? cancelOnError}) {
      final subs = LightSubscription<T>(
        removeSubscription,
        onPause: onPause,
        onResume: onResume,
        onCancel: onCancel,
      )
        ..onData(onData)
        ..onError(onError)
        ..onDone(onDone)
        ..cancelOnError = cancelOnError;
      addSubscription(subs);
      onListen?.call();
      return subs;
    }
  
   // 把LightSubscription添加到List中等待被临幸 
   FutureOr<void> addSubscription(LightSubscription<T> subs) async {
    if (!_isBusy!) {
      return _onData!.add(subs);
    } else {
      await Future.delayed(Duration.zero);
      return _onData!.add(subs);
    }
  }
}

回到 _ObxState类,在 #6 build方法中,并没有直接返回widget.build() 直接返回新创建的Widget而是调用了RxInterface的静态方法notifyChildren,这个就是上面的 #预留问题2 ,我们继续分析一下这个方法:

static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
  
    final _observer = RxInterface.proxy;
    RxInterface.proxy = observer;
    final result = builder();
    if (!observer.canUpdate) {
      RxInterface.proxy = _observer;
      /// 这个就是使用Obx中并没有用到obs修饰的变量,而抛出的异常,用过getx的都知道这个异常😹
      throw """
      [Get] the improper use of a GetX has been detected. 
      You should only use GetX or Obx for the specific widget that will be updated.
      If you are seeing this error, you probably did not insert any observable variables into GetX/Obx 
      or insert them outside the scope that GetX considers suitable for an update 
      (example: GetX => HeavyWidget => variableObservable).
      If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
      """;
    }
    RxInterface.proxy = _observer;
    return result;
}

这个方法调用时做了一个校验,如果使用如下方式使用Obx,则会抛出异常。

child:Obx(() => Text("计数结果"))
///这时就会抛出异常,因为Obx中没有可观察的对象。

这么做的初衷应该是想要规避那些不正确的写法导致增加内存,因为Obx是继承自StatefulWidget的,而上面的写法完全可以用StatelessWidget实现。

通过上面的一顿操作,我们已经知道此时我们创建Obx对象中已经有一个订阅者StreamSubscription在等待通知,那通知什么时候会来呢?我们继续分析一下个步骤。

obs和Obx的绑定

经过上面的分析,我们已经大致知道了obsObx的实现逻辑,那obs的数据是如何最终绑定到Obx里呢?我们先回顾一下Demo里面你的这段代码:

body: Center(
   child: Obx(() => Text("计数结果 = ${counter.value}")),
)

接下来跟踪counter.value方法:

T get value {
  RxInterface.proxy?.addListener(subject);
  return _value;
}

赫然发现,这个方法就是我们上面的 #预留问题1,这个地方有两个问题: ① RxInterface.proxy对象是什么?

② 参数subject是什么?

问题②比较好解答,细心的同学应该还记得我们创建的RxInt对象是继承自NotifyManager的,而NotifyManager中就有个GetStream类型的属性subject

问题①比较难,因为要从整个CounterDemoPage的加载说起,整个流程如下:

1、CounterDemoPage对象创建,作为属性的counter随后完成创建,counter父类为RxNotifier(1)

2、CounterDemoPagebuild的方法被调用,Obx对象被创建,Obx中持有一个Function,返回值为Widget

3、Obx的父类ObxWidget_ObxState被创建, _ObxState持有了对象RxNotifier(2)

4、 _ObxStatebuild方法被调用,调用到RxInterface.notifyChildren方法,该p1为 _ObxState持有的对象RxNotifier(2) ,p2为Obx中的Function

5、执行notifyChildren方法

/// 当前Obx中持有的obs对象
final _subscriptions = <GetStream, List<StreamSubscription>>{};
​
static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    /// 此时RxInterface.proxy对象为null,将null值赋给_observer
    final _observer = RxInterface.proxy;
    /// 把RxNotifier(2)对象复给RxInterface.proxy,此时proxy短暂不为空
    RxInterface.proxy = observer;
    /// 调用Obx中持有的函数Function,执行步骤6
    final result = builder();
  
    /// 判断是否能更新,如果不能抛出异常,判断依据就是_subscriptions是否为空
    if (!observer.canUpdate) {
      RxInterface.proxy = _observer;
      throw ""
    }
  
    /// proxy对象重新置空,开始下一个轮回
    RxInterface.proxy = _observer;
    /// Obx的Function返回值作为该方法的最终返回值
    return result;
}

6、Obx中的函数体为return Text("计数结果 = ${counter.value}") ,这便回到了本小节刚开始的地方,调用counter.value方法

结论:问题①中的RxInterface.proxy对象就是我们创建的Obx对象的父类的state _ObxState中持有的RxNotifier(2)

#预留问题1解决了,我们继续调用addListener方法,也就是我们上面的 #预留问题3,我们继续跟进问题:

/// 当前Obx中持有的obs对象
final _subscriptions = <GetStream, List<StreamSubscription>>{};

/// 该方法就是把RxInt中的subject与Obx中的subject做关联 
/// 参数rxGetx为RxInt中的subject
void addListener(GetStream<T> rxGetx) {
  /// 判断当前RxInt是否已经添加过了,避免重复添加
  if (!_subscriptions.containsKey(rxGetx)) {
    /// 在这个时候,RxInt才开始监听value数据的变化
    final subs = rxGetx.listen((data) {
      /// RxInt中数据变化的通知直接转发到Obx中的GetStream中去
      if (!subject.isClosed) subject.add(data);
    });
    /// 将RxInt中listen方法的返回值StreamSubscription类型的subs添加到_subscriptions中,作为canUpdate方法的判断依据
    final listSubscriptions = _subscriptions[rxGetx] ??= <StreamSubscription>[];
    listSubscriptions.add(subs);
  }
}

看到这里,终于明白RxInt中的RxNotifier(1)Obx中的RxNotifier(2) 是如何做到联动通知的了,Obx的被通知数据有更新后,触发了RxNotifier(2) 中持有的 _updateTree函数,然后以setState的方式重走了 _ObxStatebuild方法,最终完成了UI的更新。

在我们点击按钮时,触发RxIntvalue方法,也是执行相同的逻辑。

从上面的分析可的出,如果一个obs创建的Rx对象,没有在任何Obx中使用到,那么该对象不会添加监听方法,节约了内存开销,如果Obx没有使用任何Rx对象,则会在开发阶段抛出异常,提醒开发者不规范的操作。在RxNotifier(1)RxNotifier(2) 的引用方式也保证了消息不会被错误的通知到,不得不说这是一个很有意思的设计。

结案陈词

最后用简单的方式总结一下状态管理机制的实现逻辑:

1、通过obs的方式创建RxInt类型的待观察数据counter,创建了stream对象但没有被订阅。

2、创建Obx并使用了counter,在Obxbuild第一次被调用时,Obx中的streamcounterstream设置了订阅事件,并把counter中观察到的变化同步给Obx

3、Obxstream观察到变化,调用Obx中传入的函数参数,完成UI的刷新。

4、以上。

写在最后

原创不易,您的关注和点赞是我最大的动力~