看了Flutter get状态库源码,我调整了编码姿势

前言

好久没有写源码分析相关的文章了,之前快速上手了get状态库,现在回头看了源码,以前没想通的都开窍了,使用不当的地方也做了调整。
通过本篇文章,你将了解到:

  1. 什么是状态管理?
  2. 设计模式之观察者模式
  3. get 如何实现状态管理?
  4. Obx和obs如何配合?
  5. get 使用建议

1. 什么是状态管理?

比如我们有个UI需要显示当前分数第一名的同学:

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

class NumberOne extends StatelessWidget {
  final String name = "fish";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('摸鱼排行榜'),
      ),
      body: Center(
        child: Text(
          '第一名: $name',
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

此时的UI是写死的,第一名永远是"fish",UI状态是固定的,不需要管理。
然而,排行榜是动态变化的,显示的第一名需要变化,改造如下:

class NumberOne extends StatefulWidget {
  @override
  _NumberOneState createState() => _NumberOneState();
}

class _NumberOneState extends State<NumberOne> {
  String name = "fish";

  void _refreshLeaderboard() {
    setState(() {
      // 这里可以添加刷新排行榜的逻辑
      name = "fish${Random().nextInt(100)}"; // 示例:更新名字
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('摸鱼排行榜'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '第一名: $name',
              style: TextStyle(fontSize: 24),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _refreshLeaderboard,
              child: Text('刷新排行榜'),
            ),
          ],
        ),
      ),
    );
  }
}

当点击按钮"刷新排行榜"时,第一名的名字随机变化,同时在UI上展示最新的名字。
此处涉及到了两个知识点:StatefulWidget和State。
StatefulWidget 表示该Widget是有状态,它的状态通过State来体现。
"有状态"的意思是UI的状态是可变的,当刷新排行榜时,实际是发生了两件事:

  1. 修改了变量name的值
  2. 通过调用setState,触发了组件的build方法进行重绘

setState里的逻辑是:先回调了执行闭包代码(修改name),再触发组件重绘。

将_refreshLeaderboard方法改造如下更好理解:

 void _refreshLeaderboard() {
    // 这里可以添加刷新排行榜的逻辑
    name = "fish${Random().nextInt(100)}"; // 示例:更新名字
    setState(() {
    });
  }

数据驱动UI状态变化,管理UI状态变化的过程称为状态管理,setState是Flutter里最基础的状态管理方式

2. 设计模式之观察者模式

虽然setState能够实现状态管理,试想一下:如果UI状态比较复杂,依赖很多数据,每次每个数据变化都需要显式写一下setState闭包逻辑,想想都比较麻烦,那是否可以将setState封装起来,当数据变化的时候自动调用setState方法,如此一来只需要关注数据变化即可。
这场景符合设计模式里的观察者模式。


image.png

实现观察者模式有两个核心点:

  1. 观察者(observer)向被观察者(subject)注册监听(回调)
  2. 被观察者状态变化后通过回调通知观察者

由此可知,UI属于观察者,数据属于被观察者,当UI向数据注册观察者,数据变化之后通知UI,而后UI调用setState方法,一次自动状态管理就完成了。

3. get 如何实现状态管理?

为了更好地处理状态管理,许多优秀的三方框架大显神通,本次我们主要分析其中的佼佼者:get。
将上面的NumberOne改造如下:

class NumberOne extends StatelessWidget {
  String name = "fish";
  final controller = NumberOneController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('摸鱼排行榜'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GetBuilder<NumberOneController>(
                builder: (_) => Text(
                      '第一名: ${controller.name}',
                      style: TextStyle(fontSize: 24),
                    ),
                init: controller),
            ElevatedButton(
              child: Text('刷新排行榜'),
              onPressed: controller.updateName,
            ),
          ],
        ),
      ),
    );
  }
}
class NumberOneController extends GetxController {
  var name = "fish";

  void updateName() {
    name = "fish${Random().nextInt(100)}";
    update();
  }
}

当点击"刷新排行榜"时,UI自动更新了,此时并没有看到任何地方调用了setState。
当需要观察UI变化时,只需要使用GetBuilder包裹对应的组件,然后数据都定义在Controller里,当数据发生变化时更新controller即可自动刷新UI。
接下来深入源码探索其实现。


image.png

GetBuilder 继承自StatefulWidget,传入的builder就是用来构造目标组件。
当State调用到initState时:

_subscribeToController-->controller?.addListener(getUpdate)-->_updaters!.add(listener)

_updaters是数组,存储着listener(getUpdate)。

getUpdate实现如下:

mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
  // To avoid the creation of an anonym function to be GC later.
  // ignore: prefer_function_declarations_over_variables

  /// Experimental method to replace setState((){});
  /// Used with GetStateUpdate.
  void getUpdate() {
    if (mounted) setState(() {});
  }
}

看到了熟悉的setState。
也就是说GetBuilder作为观察者,Controller作为被观察者,GetBuilder向Controller注册了监听getUpdate,当Controller发生变化时会调用getUpdate,而该方法里有setState,如此便达到了刷新的目的。

当Controller调用:

updateName-->update-->refresh-->_notifyUpdate

实现如下:

  void _notifyUpdate() {
    for (var element in _updaters!) {
      element!();
    }
  }

_updaters是数组,存储着上面的getUpdate,element!()即是执行getUpdate。

image.png

GetBuilder X Controller 即是典型的观察者模式的实现。

4. Obx和obs如何配合?

GetBuilder X Controller 配合下,虽然已经不用显式调用setState,但还是需要额外调用Controller.update方法,还是有优化的空间。
此时轮到Obx和obs出场,继续改造上面的NumberOne。

class NumberOne extends StatelessWidget {
  String name = "fish";
  final numberOneEntity = NumberOneEntity();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('摸鱼排行榜'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Obx(() => Text(
                  '第一名: ${numberOneEntity.name}',
                  style: TextStyle(fontSize: 24),
                )),
            ElevatedButton(
              child: Text('刷新排行榜'),
              onPressed: () {
                numberOneEntity.name.value = "fish${Random().nextInt(100)}";
              },
            ),
          ],
        ),
      ),
    );
  }
}

class NumberOneEntity {
  var name = "fish".obs;
}

此时无需继承自Controller,也无需显式调用Controller.update方法,仅仅只需要聚焦于数据的改变。

虽然Obx+obs写起来比GetBuilder+Controller简单很多,但前者原理比后者复杂不少。

依然是观察者模式,只是有两层的观察者模式。

  1. Obx是观察者,obs是被观察者,当obs发生变化时通知Obx刷新UI
  2. Obx、obs内部分别有观察者和被观察者

接着分析其源码,先看Obx。
Obx 继承自StatefulWidget,核心是ObxState,重点关注里面的一个属性和三个方法:

  final _observer = RxNotifier();

  void initState() {
    super.initState();
    subs = _observer.listen(_updateTree, cancelOnError: false);
  }

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

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

_observer.listen:Obx内部的观察者向被观察者添加监听。
_updateTree 很眼熟,它的实现就是调用了setState方法,根据之前的经验,我们需要按图索骥看谁最终调用了_updateTree即可,也就是说找到被观察者是谁。

  StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,
      );

  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;
  }

此处的subject(GetStream对象)就是被观察者,onData对应的是传入的_updateTree。

listen方法里构造LightSubscription对象,onData存储到了LightSubscription对象里,而后将LightSubscription对象存储在GetStream维护的数组:_onData里:


image.png

至此,理出来的逻辑为:

Obx的State里持有RxNotifier(_observer)-->GetStream(subject)-->List<LightSubscription>(_onData)-->OnData(_data)
_data==_updateTree
括号里是对应的变量名

Obx里的观察者模式(ObxState是观察者,LightSubscription是被观察者)已经搭建起来了,就看是什么时候被触发了。

当Obx State.build执行时会调用RxInterface.notifyChildren(_observer, widget.build)。

  static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
    final oldObserver = RxInterface.proxy;
    RxInterface.proxy = observer;
    final result = builder();
    if (!observer.canUpdate) {
      RxInterface.proxy = oldObserver;
      throw """
    }
    RxInterface.proxy = oldObserver;
    return result;
  }

核心两点:

  1. RxInterface.proxy = observer,将Obx里的_observer存储到静态变量里
  2. 执行Obx的闭包,也就是Obx包裹的组件
  3. 将静态变量指向老的对象(为null),用以解除全局对象持有UI,避免内存泄漏

以上Obx的主要内容结束了。
接着看obs相关内容。

class NumberOneEntity {
  var name = "fish".obs;
}

"fish".obs 是借助了语言上的扩展功能:

extension StringExtension on String {
  /// Returns a `RxString` with [this] `String` as initial value.
  RxString get obs => RxString(this);
}

基本类型:String、int、bool、double,引用类型都有对应的扩展,核心代码是在父类_RxImpl里。

abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> 

继承自RxNotifier,Obx里也持有了RxNotifier的示例。

 Obx(() => Text(
                  '第一名: ${numberOneEntity.name}',
                  style: TextStyle(fontSize: 24),
                )),

当Obx执行build之后,就开始创建Text对象,而Text构造函数入参是需要访问numberOneEntity.name,访问的过程实际就是取name的值,name是_RxImpl类型的,实际的值存储在_value里,因此取name的值就是取_value的值,而_RxImpl定义了_value 的get方法:

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

get方法除了返回obs的具体值,还多了一个addListener的操作。
RxInterface.proxy 的值就是Obx State里的_observer,而此处的subject是_RxImpl里的成员变量,它是GetStream类型的。

  void addListener(GetStream<T> rxGetx) {
    //当前的对象是Obx里的_observer
    //_subscriptions是该Obx维护的数组,因此一个Obx下面可能监听多个obs
    //每个obs可能会触发多次get,而后触发addListener,此处做过滤
    if (!_subscriptions.containsKey(rxGetx)) {
      //重点是这
      //rxGetx 是obs的成员变量,它的内部订阅了观察,和Obx的订阅流程类似
      final subs = rxGetx.listen((data) {
        //当obs改变时,会调用此方法,而subject是属于Obx里的
        if (!subject.isClosed) subject.add(data);
      });
      final listSubscriptions =
      _subscriptions[rxGetx] ??= <StreamSubscription>[];
      listSubscriptions.add(subs);
    }
  }

Obx(观察者)和obs(被观察者),两者的连接是在addListener方法里。
继续看subject.add(data)

  void add(T event) {
    assert(!isClosed, 'You cannot add event to closed Stream');
    _value = event;
    _notifyData(event);
  }
  void _notifyData(T data) {
    _isBusy = true;
    for (final item in _onData!) {
      if (!item.isPaused) {
        item._data?.call(data);
      }
    }
    _isBusy = false;
  }

_onData是订阅的数组,里面存放的是观察者的回调,而对于Obx来说,其实存放的就是_updateTree。
因此,上述的逻辑可以归纳为:

obs监听了内部被观察者,当obs的回调被触发后,它会调用Obx的被观察者,而Obx的被观察者会调用其观察者,最后setState

现在就只差最后一个环节:obs的被观察者是什么时候触发的?

再来看刷新排行榜的操作:

            ElevatedButton(
              child: Text('刷新排行榜'),
              onPressed: () {
                numberOneEntity.name.value = "fish${Random().nextInt(100)}";
              },
            ),

当点击按钮后,会给obs赋值,那么来追踪一下赋值过程,依然是在_RxImpl里。

  set value(T val) {
    if (subject.isClosed) return;
    sentToStream = false;
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;
    sentToStream = true;
    subject.add(_value);
  }

除了传统的赋值(给obs里的值_value赋值),还多了一些其它操作,其中重要的是subject.add(_value)。
前面分析过Obx里的subject.add(data),而此时的subject(被观察者)是obs里的对象,两者的目的都是一样的:

通知各自的观察者

subject.add(_value)最后会调用到obs rxGetx.listen((data) 的闭包里:

 final subs = rxGetx.listen((data) {
        //当obs改变时,会调用此方法,而subject是属于Obx里的
        if (!subject.isClosed) subject.add(data);
      });

也即是通知了Obx的被观察者,Obx的被观察者再通知自己的观察者,最终setState,如此一来就完成了:

数据变更,触发UI刷新的流程

Obx、obs源码看起来比较绕的原因是它俩都依赖于NotifyManager,而NotifyManager里又封装了观察者模式,用一张图展示整体的流程:

image.png

红色部分是Obx和obs连接的关键点。
总体来看:

  1. Obx作为观察者,观察obs的变化
  2. 一个Obx里可以观察多个obs
  3. 一个obs可以被多个Obx观察

5. get 使用建议

第1点:Obx观察最小化原则


image.png

如上图,只需要观察第一个Text。

image.png

如上图,为了方便,包裹了整个Column,此种做法不推荐,理由是当name发生变化时会引发Obx的重建,最终第二个Text也会重建,浪费渲染时间和资源。

第2点:
GetX、GetBuilder 比较少用,Obx+obs组合可以替代它们。

第3点:
GetxController 并不是必须的,通常和GetView、GetBuilder搭配使用。

第4点:
和第一点类似,Obx不建议嵌套Obx。

好了,状态管理就讲到这里了。
你是否也在使用get呢?还是其它的框架?亦或是setState一把梭?
说说你遇到的坑吧~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容