Scoped 、 Provider 与 Get的状态管理

1. Scoped

Scoped 是使用了 AnimatedBuilder, 其原理是Listenable对象发出通知后, AnimatedBuilder调用state.setState().

// _AnimatedState
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (widget.listenable != oldWidget.listenable) {
    oldWidget.listenable.removeListener(_handleChange);
    widget.listenable.addListener(_handleChange);
  }
}
void _handleChange() {
  setState(() {
    // The listenable's state is our build state, and it changed already.
  });
}

2. Provider

Provider 则是Listenable对象发出通知后, 监听者调用element.markNeedsNotifyDependents()函数将Builder对应的Element标记为dirty, 在下一帧触发对应的ElementperformRebuild(), 在performRebuild()中调用built = build(), 而build()函数如下:

Widget build(BuildContext context) => builder(context);

调用了我们使用 Provider 时传入的builder.

3. Get

不管是 Scoped 还是 Provider, 都是使用InheritedWidget持有数据, InheritedWidget是通过每个Element都持有一个_ineritedWidgetsmap 在整个 APP 内同步数据, 或许我们可以自己实现一套机制, 需要满足以下条件.

  1. 通过少量条件即可在期望范围内的任何地方获得数据的持有者或者数据本身
  2. 数据改变后可以通知到 UI 进行改变

如果想要实现任何地方都能获得数据, 无疑使用单利或者全局的 map 比较好, 而需要数据修改后通知UI进行改变, 则Publisher-Subscriber模式也是一个优秀的选择.

很巧, Get的GetBuilder 正好符合以上期望.

Get 使用 GetInstance_singl持有数据模型,

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

与 InheritedWidget 和 Scoped 通过Inherited持有数据模型不同的是, GetBuilder 是通过 GetInstance 单例持有需要共享数据. 这就造成GetBuilder获取模型需要考虑同级节点相同类型的问题, 除了使用模型的类型T, 还需要一个tag.

GetBuilder 中的tag是可选的, 但在同级节点都使用相同类型的 Model 的时候必须使用tag, 比如ListView

虽然每个Element都持有_ineritedWidgets, 但父节点的_ineritedWidgets都是被包含在子节点的_ineritedWidgets中的, 逻辑上其实是一个树结构, 通过类型T是不会拿到同级节点相同类型T的数据的.

Get 中的GetBuilder 通知 UI 刷新则使用的是与AnimatedBuilder相同的原理, 收到 Model的通知后调用state.setState().

Get 除了GetBuilder之外还有GetX的响应式状态管理器, 在使用层面, 两者的区别是前者需要明确调用GetxController.update()触发UI刷新, 而GetX则只需要给需要监听的数据加上.obs.

4. GetX 原理

a. Obx对_boserver的监听

使用 GetX 时, 我们需要使用Obx组件, 其集成自ObxWidget, 代码如下:

abstract class ObxWidget extends StatefulWidget {
  const ObxWidget({Key? key}) : super(key: key);

  @override
  _ObxState createState() => _ObxState();

  @protected
  Widget build();
}

class _ObxState extends State<ObxWidget> {
  final _observer = RxNotifier();
  late StreamSubscription subs;

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

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

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

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

可以看到ObxWidget持有一个_ObxState, _ObxState持有final _observer = RxNotifier(), 这就是一个通知者(或者说发布者), 在initState()中对_observer做了监听.

RxNotifier内部持有GetStream<T> subject = GetStream<T>(), 我们其实是对subject做了监听, 当subject.add(event)被调用时就会触发监听.

b. _observerRxObject的监听.

作为响应式状态管理方案, 响应式对象, 即我们想要监听的数据可称为RxObject. 下面代码中count就是一个RxObject.

var count = 0.obs;

作为一个响应式方案, GetX中数据源也是RxNotifier, 但RxNotifier不一定是数据源, 这样应该是为了组成响应链.
但如果一个RxNotifier作为数据源, 那就需要具备一些独有的能力, 在GetX中以_RxImplRxObjectMixin实现的, 其中_RxImpl及一些列子类用来实现数据存储和计算功能.
RxObjectMixin则主要实现数据源的事件触发的相关功能能, 所以GetX的事件都来自RxObjectMixin.
这里将所有 with RxObjectMixin 的对象称为RxObject.

_observer不是RxObject, 它即不持有数据, 也不会发出事件, GetX_observerRxObject做了监听, 实现在_observer.addListener()方法中, 由RxObject主动调起.

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

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

  bool get canUpdate => _subscriptions.isNotEmpty;

  // RxObject调用这个方法, 这里实现了_oberver对RxObject的监听
  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);
    }
  }

  // Obx调用这个方法, 监听_observer.
  StreamSubscription<T> listen(
    void Function(T) onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) =>
      subject.listen(
        onData,
        onError: onError,
        onDone: onDone,
        cancelOnError: cancelOnError ?? false,
      );

  /// Closes the subscriptions for this Rx, releasing the resources.
  void close() {
    _subscriptions.forEach((getStream, _subscriptions) {
      for (final subscription in _subscriptions) {
        subscription.cancel();
      }
    });

    _subscriptions.clear();
    subject.close();
  }
}

_observer.addListener()是由RxObject主动调起的. 具体过程是在Obx首次创建时, 会调用build()函数, 实际调用了RxInterface.notifyChildren(), 并传入_observer.

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

RxInterface.notifyChildren()中, 将_observer赋值给RxInterface.proxy, 这么做主要是为了在RxObject通过访问静态变量RxInterface.proxy就可以获得对应的_observer.

static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
  final _observer = RxInterface.proxy;
  RxInterface.proxy = observer;
  // 这里触发 RxObject.value 的 get 方法
  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;
}

final result = builder()这行代码中, 调用了我们传入Obxbuilder, 一般builder可以这样写:

Obx(() {
  return Text('count: $count');
})

这最终调用了RxObject.valueget方法.

定义一个RxObject: RxObject obj, 当我们在表达式中直接使用obj其实是调用了这个类型的call()函数, 而$obj则相当于obj.call().toString().

RxObject的关键代码:

T call([T? v]) {
  if (v != null) {
    value = v;
  }
  return value;
}

String get string => value.toString();
@override
String toString() => value.toString();
dynamic toJson() => value;


set value(T val) {
  if (subject.isClosed) return;
  sentToStream = false;
  if (_value == val && !firstRebuild) return;
  firstRebuild = false;
  _value = val;
  sentToStream = true;
  subject.add(_value);
}
/// Returns the current [value]
T get value {
  RxInterface.proxy?.addListener(subject);
  return _value;
}

通过call()函数, 我们触发了T get value方法. 然后调动了RxInterface.proxy?.addListener(subject), 最终完成了_observerRxObject的监听.

c. 发送事件

通过 a 和 b 两步, 已经完成了整个监听链条, 最终则是触发事件, 或者说发送事件.

RxInt 为例, 我们如果监听一个int类型的数据, 需要写为var num = 0.obs, obs函数返回的就是一个RxInt对象(集成自RxObject), 其内部实现了int类型的运算, 下面是加法运算:

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

其中value = value + othervalue做了赋值, 毫无疑问会触发RxObject.valueset方法, 当我们调用num++, 最终会在set方法中调用subject.add(_value), 完成了事件发送.

监听链:


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

推荐阅读更多精彩内容