Scoped 饵逐、 Provider 與 Get的狀態(tài)管理

1. Scoped

Scoped 是使用了 AnimatedBuilder, 其原理是Listenable對象發(fā)出通知后, AnimatedBuilder調(diào)用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對象發(fā)出通知后, 監(jiān)聽者調(diào)用element.markNeedsNotifyDependents()函數(shù)將Builder對應(yīng)的Element標(biāo)記為dirty, 在下一幀觸發(fā)對應(yīng)的ElementperformRebuild(), 在performRebuild()中調(diào)用built = build(), 而build()函數(shù)如下:

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

調(diào)用了我們使用 Provider 時傳入的builder.

3. Get

不管是 Scoped 還是 Provider, 都是使用InheritedWidget持有數(shù)據(jù), InheritedWidget是通過每個Element都持有一個_ineritedWidgetsmap 在整個 APP 內(nèi)同步數(shù)據(jù), 或許我們可以自己實現(xiàn)一套機制, 需要滿足以下條件.

  1. 通過少量條件即可在期望范圍內(nèi)的任何地方獲得數(shù)據(jù)的持有者或者數(shù)據(jù)本身
  2. 數(shù)據(jù)改變后可以通知到 UI 進行改變

如果想要實現(xiàn)任何地方都能獲得數(shù)據(jù), 無疑使用單利或者全局的 map 比較好, 而需要數(shù)據(jù)修改后通知UI進行改變, 則Publisher-Subscriber模式也是一個優(yōu)秀的選擇.

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

Get 使用 GetInstance_singl持有數(shù)據(jù)模型,

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

與 InheritedWidget 和 Scoped 通過Inherited持有數(shù)據(jù)模型不同的是, GetBuilder 是通過 GetInstance 單例持有需要共享數(shù)據(jù). 這就造成GetBuilder獲取模型需要考慮同級節(jié)點相同類型的問題, 除了使用模型的類型T, 還需要一個tag.

GetBuilder 中的tag是可選的, 但在同級節(jié)點都使用相同類型的 Model 的時候必須使用tag, 比如ListView

雖然每個Element都持有_ineritedWidgets, 但父節(jié)點的_ineritedWidgets都是被包含在子節(jié)點的_ineritedWidgets中的, 邏輯上其實是一個樹結(jié)構(gòu), 通過類型T是不會拿到同級節(jié)點相同類型T的數(shù)據(jù)的.

Get 中的GetBuilder 通知 UI 刷新則使用的是與AnimatedBuilder相同的原理, 收到 Model的通知后調(diào)用state.setState().

Get 除了GetBuilder之外還有GetX的響應(yīng)式狀態(tài)管理器, 在使用層面, 兩者的區(qū)別是前者需要明確調(diào)用GetxController.update()觸發(fā)UI刷新, 而GetX則只需要給需要監(jiān)聽的數(shù)據(jù)加上.obs.

4. GetX 原理

a. Obx對_boserver的監(jiān)聽

使用 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(), 這就是一個通知者(或者說發(fā)布者), 在initState()中對_observer做了監(jiān)聽.

RxNotifier內(nèi)部持有GetStream<T> subject = GetStream<T>(), 我們其實是對subject做了監(jiān)聽, 當(dāng)subject.add(event)被調(diào)用時就會觸發(fā)監(jiān)聽.

b. _observerRxObject的監(jiān)聽.

作為響應(yīng)式狀態(tài)管理方案, 響應(yīng)式對象, 即我們想要監(jiān)聽的數(shù)據(jù)可稱為RxObject. 下面代碼中count就是一個RxObject.

var count = 0.obs;

作為一個響應(yīng)式方案, GetX中數(shù)據(jù)源也是RxNotifier, 但RxNotifier不一定是數(shù)據(jù)源, 這樣應(yīng)該是為了組成響應(yīng)鏈.
但如果一個RxNotifier作為數(shù)據(jù)源, 那就需要具備一些獨有的能力, 在GetX中以_RxImplRxObjectMixin實現(xiàn)的, 其中_RxImpl及一些列子類用來實現(xiàn)數(shù)據(jù)存儲和計算功能.
RxObjectMixin則主要實現(xiàn)數(shù)據(jù)源的事件觸發(fā)的相關(guān)功能能, 所以GetX的事件都來自RxObjectMixin.
這里將所有 with RxObjectMixin 的對象稱為RxObject.

_observer不是RxObject, 它即不持有數(shù)據(jù), 也不會發(fā)出事件, GetX_observerRxObject做了監(jiān)聽, 實現(xiàn)在_observer.addListener()方法中, 由RxObject主動調(diào)起.

// 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調(diào)用這個方法, 這里實現(xiàn)了_oberver對RxObject的監(jiān)聽
  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調(diào)用這個方法, 監(jiān)聽_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主動調(diào)起的. 具體過程是在Obx首次創(chuàng)建時, 會調(diào)用build()函數(shù), 實際調(diào)用了RxInterface.notifyChildren(), 并傳入_observer.

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

RxInterface.notifyChildren()中, 將_observer賦值給RxInterface.proxy, 這么做主要是為了在RxObject通過訪問靜態(tài)變量RxInterface.proxy就可以獲得對應(yīng)的_observer.

static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
  final _observer = RxInterface.proxy;
  RxInterface.proxy = observer;
  // 這里觸發(fā) 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()這行代碼中, 調(diào)用了我們傳入Obxbuilder, 一般builder可以這樣寫:

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

這最終調(diào)用了RxObject.valueget方法.

定義一個RxObject: RxObject obj, 當(dāng)我們在表達式中直接使用obj其實是調(diào)用了這個類型的call()函數(shù), 而$obj則相當(dāng)于obj.call().toString().

RxObject的關(guān)鍵代碼:

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()函數(shù), 我們觸發(fā)了T get value方法. 然后調(diào)動了RxInterface.proxy?.addListener(subject), 最終完成了_observerRxObject的監(jiān)聽.

c. 發(fā)送事件

通過 a 和 b 兩步, 已經(jīng)完成了整個監(jiān)聽鏈條, 最終則是觸發(fā)事件, 或者說發(fā)送事件.

RxInt 為例, 我們?nèi)绻O(jiān)聽一個int類型的數(shù)據(jù), 需要寫為var num = 0.obs, obs函數(shù)返回的就是一個RxInt對象(集成自RxObject), 其內(nèi)部實現(xiàn)了int類型的運算, 下面是加法運算:

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

其中value = value + othervalue做了賦值, 毫無疑問會觸發(fā)RxObject.valueset方法, 當(dāng)我們調(diào)用num++, 最終會在set方法中調(diào)用subject.add(_value), 完成了事件發(fā)送.

監(jiān)聽鏈:


getx.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荣月,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子梳毙,更是在濱河造成了極大的恐慌,老刑警劉巖捐下,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件账锹,死亡現(xiàn)場離奇詭異,居然都是意外死亡坷襟,警方通過查閱死者的電腦和手機奸柬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婴程,“玉大人廓奕,你說我怎么就攤上這事〉凳澹” “怎么了桌粉?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衙四。 經(jīng)常有香客問我铃肯,道長,這世上最難降的妖魔是什么传蹈? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任押逼,我火速辦了婚禮步藕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挑格。我一直安慰自己咙冗,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布漂彤。 她就那樣靜靜地躺著雾消,像睡著了一般。 火紅的嫁衣襯著肌膚如雪显歧。 梳的紋絲不亂的頭發(fā)上仪或,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音士骤,去河邊找鬼范删。 笑死,一個胖子當(dāng)著我的面吹牛拷肌,可吹牛的內(nèi)容都是我干的到旦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼巨缘,長吁一口氣:“原來是場噩夢啊……” “哼添忘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起若锁,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤搁骑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后又固,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仲器,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年仰冠,在試婚紗的時候發(fā)現(xiàn)自己被綠了乏冀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡洋只,死狀恐怖辆沦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情识虚,我是刑警寧澤肢扯,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站担锤,受9級特大地震影響鹃彻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妻献,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一蛛株、第九天 我趴在偏房一處隱蔽的房頂上張望团赁。 院中可真熱鬧,春花似錦谨履、人聲如沸欢摄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怀挠。三九已至,卻和暖如春害捕,著一層夾襖步出監(jiān)牢的瞬間绿淋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工尝盼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吞滞,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓盾沫,卻偏偏與公主長得像裁赠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子赴精,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容