GetX状态管理原理分析

GetX, pub.dev 评分 11535,目前已经超越了provider,成为了flutter中最火的状态管理框架.GetX现在包含的内容很多,这篇主要分析一下其中状态管理的原理.

image.png

首先GetX的依赖注入是getx状态管理的关键,这一步保证了数据的全局访问能力,不受制于节点(对比InherentedWidget),只要被注入,全局可访问,通过getx源码可以看到

  S put<S>(
    S dependency, {
    String? tag,
    bool permanent = false,
    @deprecated InstanceBuilderCallback<S>? builder,
  }) {
    _insert(
        isSingleton: true,
        name: tag,
        permanent: permanent,
        builder: builder ?? (() => dependency));
    return find<S>(tag: tag);
  }

  void _insert<S>({
    bool? isSingleton,
    String? name,
    bool permanent = false,
    required InstanceBuilderCallback<S> builder,
    bool fenix = false,
  }) {
    // 这一步使用了Type+tag作为key
    final key = _getKey(S, name);

    // 这一步就是将传入的GetController保存起来
    if (_singl.containsKey(key)) {
      final dep = _singl[key];
      if (dep != null && dep.isDirty) {
        _singl[key] = _InstanceBuilderFactory<S>(
          isSingleton,
          builder,
          permanent,
          false,
          fenix,
          name,
          lateRemove: dep as _InstanceBuilderFactory<S>,
        );
      }
    } else {
      _singl[key] = _InstanceBuilderFactory<S>(
        isSingleton,
        builder,
        permanent,
        false,
        fenix,
        name,
      );
    }
  }

这里GetX使用了一个单例

class GetInstance {
  factory GetInstance() => _getInstance ??= const GetInstance._();

  const GetInstance._();

  static GetInstance? _getInstance;

  static final Map<String, _InstanceBuilderFactory> _singl = {};

因此不管是在哪里,都可以获取到这个_singl,也就实现了全局访问的功能

接下来就是如何使用这个GetController,在UI显示上,GetX提供了两种使用方案:

  • 使用GetBuilder,手动更新状态
  • 使用Obx自动刷新

GetBuilder是一个StatefulWidget

class GetBuilder<T extends GetxController> extends StatefulWidget {

在他的State 的initState中,通过指定的泛型(Type)以及tag获取到对应的GetController(之前被注入的,或者是刚刚创建的),赋值给State中的controller属性,然后调用_subscribeToController,去将当前State的setState方法绑定到GetController上

void initState() {
    // _GetBuilderState._currentState = this;
    super.initState();
    widget.initState?.call(this);

    var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);

    if (widget.global) {
      if (isRegistered) {
        if (GetInstance().isPrepared<T>(tag: widget.tag)) {
          _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();
  }
void _subscribeToController() {
    _remove?.call();
    _remove = (widget.id == null)
        ? controller?.addListener(
            _filter != null ? _filterUpdate : getUpdate,
          )
        : controller?.addListenerId(
            widget.id,
            _filter != null ? _filterUpdate : getUpdate,
          );
  }

void getUpdate() {
    if (mounted) setState(() {});
  }

在GetController中,_updaters保存了所有使用了当前getController的State刷新回调

Disposer addListener(GetStateUpdate listener) {
    assert(_debugAssertNotDisposed());
    _updaters!.add(listener);
    return () => _updaters!.remove(listener);
  }

当需要更新数据的时候,直接在getController中调用update,这个时候就会将_updaters中的所有回调函数都执行一遍,这时候就完成了页面刷新

void update([List<Object>? ids, bool condition = true]) {
    if (!condition) {
      return;
    }
    if (ids == null) {
      refresh();
    } else {
      for (final id in ids) {
        refreshGroup(id);
      }
    }
  }

void refresh() {
    assert(_debugAssertNotDisposed());
    _notifyUpdate();
  }

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

Obx自动刷新,响应式状态管理器

这种方式只需要关注于数据,不用关心什么时候刷新的问题,因为在修改完数据后,页面会自动进行刷新
使用Obx,第一步要对数据进行"obs"修饰,本质是将数据包装了一层

final count = 0.obs;
class Rx<T> extends _RxImpl<T> {
  Rx(T initial) : super(initial);

  @override
  dynamic toJson() {
    try {
      return (value as dynamic)?.toJson();
    } on Exception catch (_) {
      throw '$T has not method [toJson]';
    }
  }
}
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {

最终的数据会被包装成RxNotifier,作用后面再说.
接下来该介绍Obx了,我们会发现Obx也是继承自StatefulWidget,所以应该可以想到这种方式最后也是通过调用State中的setState来完成页面刷新的(至少能感觉到)

class Obx extends ObxWidget {
abstract class ObxWidget extends StatefulWidget {

在ObxWidget的State中我们又发现了一个RxNotifier,上面说了数据被包装成了RxNotifier(这里需要注意,目前已经有两个RxNotifier属性了,一个是GetController中数据本身被包装成RxNotifier,一个是State中的属性_observer是一个RxNotifier)
这里我们可以给这两个RxNotifier起个名字,GetController中的叫做dataRxNotifier(D),State中的叫做observerRxNotifier(O),RxNotifier我们可以简单理解他就是一个包装类,里边有一个可以被多次订阅的Stream(subject),

final _observer = RxNotifier();
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;

然后看initState,在这里O被监听了,当O发生变化后调用_updateTree,也就是setState

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

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

到这里了,我们可以思考下,如果想要页面刷新,是不是只需要触发_observer(O)的刷新机制就可以了!!!
然后看State中的build方法

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

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

builder就是Obx中传入的builder, observer就是State中的observer,上文我们给它起名字叫做O,这里会发现一个有趣的问题,就是在执行builder之前,我们替换了RxInterface.proxy为当前State中的observer(O),然后执行完后又换了回来,看着一头雾水,其实这里真的是点睛之笔,所有的不可能在这里全部连接了起来,接着往下看,就会豁然开朗.

在我们不熟练的时候,有时候使用Obx,里边没有obs修饰的参数,这个时候getx会报异常,其实就是上边的这段话,所以getx要求,Obx中必须要包含obs修饰的数据
接下来我们就要看builder了,联想一下上边为什么要临时替换RxInterface.proxy
builder中,我们一定会用到的方法是D的value 的getter方法,例如:

Obx(() {
        return Row(
          children: [
            Text(Get
                .find<User>()
                .name
                .value),
          ],
        );
      })

我们调用了user的name,然后调用name的value getter(此时的name是一个RxNotifier)

class User extends GetxController {
  final name = "小铭".obs;
}

根据经验我们应该能想到,应该是在这里做了什么,才能保证数据修改,可以刷新我么当前的页面,所以我们进入value getter

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

在这里不光是将数据返回,还做了一件事RxInterface.proxy?.addListener(subject);
RxInterface.proxy 被使用了,这个时候我们是在D中,完全脱离了O,所以RxInterface.proxy这个属性的目的就是在D中可以使用O,通过每次临时替换,保证了D与O的对应关系的准确性,完美,通过一个静态变量解决了这种通信问题
我们看看addListener这个里边做了什么

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

rxGetx就是我们上文中D的Stream,而这里的subject,就是O的Stream
所以上边的代码可以这样写

void addListener(GetStream<T> dStream) {
    if (!_subscriptions.containsKey(dStream)) {
      final subs = dStream.listen((data) {
        if (!oStream.isClosed) oStream.add(data);
      });
      final listSubscriptions =
      _subscriptions[rxGetx] ??= <StreamSubscription>[];
      listSubscriptions.add(subs);
    }
  }

数据改变后,会调用dStream.listen中的回调函数,回调函数又执行了oStream.add(类比sink.add),又会执行oStream.listen中的回调,而 oStream.listen中的回调 正是_updateTree,也就是setState,这里思路一下子就清晰了

所以可以这样理解,GetX响应式状态管理器,其实依赖于两个Stream,一个是数据的Stream,一个是State 的Stream,数据改变时通过监听回调告诉State中的Stream,State中的Stream改变,才实现了页面的刷新.

但是其实这里的GetStream并不是真正的Stream,它实际上是将所有的回调保存在_onData中

List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];

当执行add方法的时候遍历执行_onData中的回调函数,

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

这里如果可以,我觉得直接建立value和setState的关系更加省事,说实在这两个GetStream真的有点画蛇添足了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容