Flutter之GetX狀態管理——Obx的使用及原理詳解

語言: CN / TW / HK

theme: cyanosis

我正在參與掘金創作者訓練營第4期,點選瞭解活動詳情,一起學習吧!

Obx 是 GetX 庫中結合 Rx 可觀察者物件實現狀態管理的 Widget 元件,使其在 Flutter 開發中實現狀態管理變得更加的快捷方便,讓程式碼變得更加的簡潔。

關於 GetX 的更多使用,請參考以下文章

使用

先從一個簡單的實現 Flutter 官方計數器效果的例子,感受一下是 Obx 是如何使用的:

```dart class CounterPage extends StatelessWidget {

var count = 0.obs;

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Counter")), body: Center( child: Obx(() { return Text("${count.value}", style: const TextStyle(fontSize: 50),); }), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: (){ count += 1; }, ), ); } } ```

實現效果:

obx.gif

去除上面其他干擾程式碼,核心程式碼如下:

```dart var count = 0.obs;

Obx(() { return Text("${count.value}"); });

count += 1; ```

整個實現分為三步:建立資料物件、在 Obx 中使用資料、更新資料

1、建立資料物件

如上面的例子,使用 .obs 即可建立一個可觀察者物件,這裡建立的是可觀察者物件是 Rx 型別,除了使用 .obs 建立外也可以直接使用 Rx 建立:

dart var count = Rx(0); /// or Rx<int> count = Rx(0);

Rx 是一個容器,存放的值是一個泛型,即能建立任意型別的可觀察者物件。

除了直接使用 Rx 建立外,GetX 還提供了常用型別的 Rx 封裝,如 RxIntRxDoubleRxBoolRxStringRxNumRxListRxMap 等,在上面的例子中使用 0.obs 建立的實際是 RxInt 型別。封裝後的 Rx 型別擴充套件了常見方法和操作符,在使用這些封裝型別時可與使用實際值一樣,下面看一下 RxIntRx<Int> 的區別 :

```dart /// 建立 RxInt a = RxInt(0); /// or RxInt a = 0.obs;

Rx b = Rx(0);

/// 使用 a += 1;

b += 1; /// 報錯 b.value += 1;

print(a > 0); /// true print(b > 0); /// false ```

通過上面的示例發現,RxInt 可以直接使用 += 操作符以及 > 等運算子,而 Rx<int> 卻不行,這就是封裝後的 Rx 類的方便之處,關於 RxIntRxDoubleRxBoolRxStringRxNumRxListRxMap 更多的功能大家可參考原始碼檢視,因實現都很簡單這裡就不多贅述了。

2、Obx 中使用資料

Rx 使用很簡單,呼叫 .value 即可獲取到實際資料。Rx 可單獨使用,但更多的是在 Obx 中使用實現狀態管理功能。

```dart var count = 0.obs;

int count_value = count.value; ```

3、更新資料

Rx 物件可使用 .value 重新賦值或者使用 update 進行資料更新,對於基礎資料型別:Int、bool、String、Double、num 則只能使用 .value 重新賦值進行更新,無論是 Rx<int> 還是 RxInt 都只能使用 .value 進行更新:

```dart var count = 0.obs;

count.value = 1; count += 1;

var user = User("loongwind", 0).obs;

user.value = User("loongwind", 1); user.update((value) { value?.age = 20; }); ```

當使用 .valueupdate 更新資料後,Obx 中的控制元件就會重新重新整理介面資料。

原理解析

前面將了 Obx 以及 Rx 的使用,下面將通過原始碼分析 Obx 與 Rx 實現狀態管理的原理。

在上面的使用中我們說到了 Rx 是一個可觀察者物件,那麼 Obx 必然是訂閱了 Rx 物件,監聽 Rx 值的變化,當監聽到 Rx 值發生變化時重新整理介面。那麼 Obx 是怎樣監聽 Rx 的資料變化的呢?Rx 又是怎樣在資料發生變化時通知 Obx 重新整理的?下面一步一步通過原始碼進行剖析。

為了幫助大家更好的梳理 Obx 與 Rx 原始碼的關係,我畫了一張 UML 圖,如下:

obx-uml.png

既然 Obx 的狀態管理分為訂閱和通知,那接下來就分別通過訂閱和通知來進行分析。

訂閱

從 UML 圖中發現,Obx 繼承自 ObxWidget,而 ObxWidget 則繼承自我們熟悉的 StatefulWidget,既然是 StatefulWidget 則肯定存在 State,也就是 UML 圖中的 _ObxState , 先看看 Obx 和 ObxWidget 的原始碼:

```dart class Obx extends ObxWidget { final WidgetCallback builder;

const Obx(this.builder);

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

Obx 中的程式碼比較簡單,建構函式中傳入 builder 即構建 Widget 的方法,實現了 build 呼叫傳入的 builder 方法。

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

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

@protected Widget build(); } ```

ObxWidget 是一個抽象類,只有一個 build 抽象方法,在 Obx 中實現。在 createState 裡建立了 _ObxState 類。

然後看看 _ObxState 的原始碼:

```dart class _ObxState extends State { 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); } ```

在 _ObxState 中首先建立了 RxNotifier 物件,即通知者,然後在 initState 中呼叫了其 listen 方法進行監聽,傳入的回撥方法是 _updateTree 即呼叫 setState 重新整理 Widget。看一下 RxNotifier.listen 方法原始碼:

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

RxNotifier.listen 方法中直接呼叫了 subject.listen 方法,而 subject 是 GetStream 型別,繼續跟進 GetStream.listen 原始碼:

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

GetStream.listen 方法中將傳入的引數封裝為 LightSubscription 物件,傳入的 onData 回撥方法傳入了 onData 方法,看一下其原始碼:

dart void onData(OnData<T>? handleData) => _data = handleData;

直接將其賦值給了 _data 變數。

封裝 LightSubscription 物件後呼叫了 addSubscription 方法:

dart FutureOr<void> addSubscription(LightSubscription<T> subs) async { if (!_isBusy!) { return _onData!.add(subs); } else { await Future.delayed(Duration.zero); return _onData!.add(subs); } }

addSubscription 中將封裝後的 LightSubscription 物件新增到 _onData 中,_onData 是一個 List : List<LightSubscription<T>>? _onData = <LightSubscription<T>>[]

時序圖如下:

obx-flow1.png

上面介紹的是 RxNotifier 的監聽流程,但是通過上面原始碼發現與 Rx 並沒有任何關係,接下來看看 _ObxState 的 build 方法:

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

直接呼叫 RxInterface.notifyChildren 方法,這是一個靜態方法,原始碼:

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

首先使用臨時變數 _observer 儲存 RxInterface.proxy , 然後將傳入的 observer 賦值給了 RxInterface.proxy,proxy 是一個靜態變數,定義如下:static RxInterface? proxy; , 而根據上面的 UML 圖可知 RxNotifier 實現了 RxInterface。

接著呼叫了 builder 方法,而傳入的 builder 就是使用 Obx 時傳入的 builder 引數,也就是真正構建 Widget 的方法,然後判斷 observer 是否能更新,如果不能更新丟擲異常,最後再將開始儲存的臨時的 _observer 重新賦值給 RxInterface.proxy ,再返回 builder 構建的 Widget 結果。

可能看到這裡大家會跟我最開始看這段原始碼時有同樣的疑惑,沒看到怎麼將 Obx 跟 Rx 進行關聯啊?經過本人反覆閱讀相關原始碼,發現關鍵點就在 RxInterface.proxy 上, RxInterface.proxy 是一個公開的靜態變數,說明它可能在其他地方進行呼叫,於是查詢原始碼發現其在 RxObjectMixin 的 get value 中有呼叫。

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

在這裡呼叫了 RxInterface.proxy?.addListener 新增監聽,跟進 UML 圖及原始碼發現,Rx 最終是混入了 RxObjectMixin 類,即在 Rx 的獲取資料中呼叫了 RxInterface.proxy?.addListener ,那什麼時候獲取 Rx 的資料呢?看看最開始的示例程式碼:

dart Obx(() { return Text("${count.value}"); });

沒錯,就是在 Obx 的 builder 方法裡,這就清楚了為什麼在 RxInterface.notifyChildren 方法裡是先將傳入的 observer 賦值給 proxy 然後再呼叫 builder 方法了,因為這樣在呼叫 builder 方法時呼叫了 Rx.value ,而在 get value 中呼叫了 RxInterface.proxy?.addListener ,且 addListener 傳入的 subject 是 Rx 的 GetStream, 而 proxy 是 _ObxState 裡建立的 RxNotifier。

addListener 實現是在 NotifyManager 裡,原始碼:

dart 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 也就是 GetStream,如果不包含則呼叫其 listen 方法,該方法的原始碼我們在上面分析 RxNotifier 訂閱的時候已經介紹了,就是對 GetStream 新增監聽。當資料更新時呼叫 subject.add 方法,注意這裡的 subject 是 _ObxState.observer 的 RxNotifier 裡的 subject,即當 Rx 資料更新時通知 _ObxState.observer 最終回撥到 _ObxState 的 _updateTree 方法實現訂閱功能。

訂閱時序圖如下:

obx-flow2.png

通知

訂閱分析完後,再來看通知是怎麼實現的,前面介紹了當呼叫 Rx 的 value 賦值或者 update 方法時會觸發介面重新整理,那麼就先來看看這兩個方法的原始碼。

update 的 實現是在 _RxImpl 裡:

dart void update(void fn(T? val)) { fn(_value); subject.add(_value); }

value 的賦值實現是在 RxObjectMixin 裡:

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

通過原始碼發現兩個方法最後都調了 subject.add(_value); 方法,並把最新的值作為變數傳入了,而 subject 是 GetStream 型別,其原始碼如下:

dart void add(T event) { assert(!isClosed, 'You cannot add event to closed Stream'); _value = event; _notifyData(event); }

最終呼叫 _notifyData 方法,繼續跟進原始碼:

dart void _notifyData(T data) { _isBusy = true; for (final item in _onData!) { if (!item.isPaused) { item._data?.call(data); } } _isBusy = false; }

發現最終遍歷 _onData 呼叫 item 的 _data.call ,而我們知道 _onData 是我們前面通過 addListener 調 listen 方法向其新增封裝的 LightSubscription 物件,而其 _data 就是 _ObxState._observer.listen 傳入的回撥方法也就是 _updateTree,最終呼叫 setState 方法實現介面的重新整理。

通知的時序圖如下:

obx-flow3.png

總結

通過對 Obx 和 Rx 的訂閱和通知的原始碼分析理解,加深了對 Obx 狀態管理的理解,在開發過程中更能靈活的對其使用。本文只講解了 Obx 和 Rx 關於訂閱和通知部分的原始碼,關於其更多的原始碼大家有興趣可以再深入學習,通過對原始碼的學習能使我們更好的理解 GetX 的庫,也能讓我們學到更多的知識。