Flutter 狀態管理新的實踐

導讀

本文介紹 flutter 端狀態刷新的一種新的思路和嘗試,通過 dart 的擴展屬性,定義一個觀察者模式,去更新 widget 的狀態,以及如何在 widget 的生命週期尋找一個切入點,建立訂閱關係。

01 背景介紹

在今年的敏捷團隊建設中,我通過 Suite 執行器實現了一鍵自動化單元測試。Juint 除了 Suite 執行器還有哪些執行器呢?由此我的 Runner 探索之旅開始了!

1.1 聲明式 UI

聲明式 UI 其實並不是近幾年的新技術,但是近幾年聲明式 UI 框架非常的火熱。單說移動端,跨平臺方案有:RN、Flutter。iOS 原生有:SwiftUI。android 原生有:compose。華爲的鴻蒙系統前段時間也發佈了基於 type-js 的 ArkUI 的 beta 版。可以看到聲明式 UI 是以後的前端發展趨勢。而狀態管理是聲明式 UI 框架的重要組成部分。

1.2 聲明式 UI 框架的狀態

在移動端之前的命令式 UI 框架,沒有狀態的概念。每個控件其實都是無狀態的,我們要更新 UI 需要手動的去 set。聲明式 UI 引入狀態的概念,狀態可以理解爲訂閱了控件所依賴數據的變化,當一個控件依賴的數據發生變化時,自動刷新 UI 展示。最大的優勢就是可以很方便的做到 UI 和邏輯的解耦。

02 provider 狀態管理

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕

2.1 使用方式

實現一個頁面如下:UI

圖 1 UI 實現

實現功能,當點擊 “按鈕” 的時候,更新 “你好” 這個組件,頁面部分代碼實現:

class SecondPage extends StatelessWidget {
  final _model = SecondPageModel();
  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
        create: (_) => _model,
        child: Scaffold(
          body: Container(
            alignment: Alignment.center,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Consumer<SecondPageModel>(
                  builder: (context, model, child) => Text(model.textA),
                ),
                Selector<SecondPageModel, String>(
                  builder: (context, model, child) => Text(model),
                  selector: (context, secondPageModel) => secondPageModel.textB,
                ),
                Consumer<SecondPageModel>(
                  builder: (context, model, child) => TextButton(
                    onPressed: () => model.textA = "你好",
                    child: Text("按鈕"),
                  ),
                ),
              ],
            ),
          ),
        ),
      );
}

model 部分實現:

class SecondPageModel with ChangeNotifier {
  String _textA = "hello";
  String _textB = "world";
  String get textA => _textA;
  set textA(String value) {
    _textA = value;
    notifyListeners();
  }
  String get textB => _textB;
  set textB(String value) {
    _textB = value;
    notifyListeners();
  }
}

2.2 問題和不足

點擊 “按鈕” 的時候查看頁面刷新,發現下表羅列的 Widget 都執行了刷新操作,使用 Selector 雖然被包裹的內容沒有刷新,但是需要進行校驗操作。

2.2.1 控件刷新

yPMu5P

2.2.2 問題分析

03 新的狀態管理方式實踐

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。

3.1 使用方式

實現同樣的上述頁面邏輯,代碼如下 (同樣基於 StatelessWidget 實現):

首先不需要依賴外部的 provider 提供 Model,任何想要獨立刷新的區域使用 TosObWidget 控件包裹即可,使用比較靈活,我們可以把 TosObWidget 插入到任何我們想要的位置(包括 provider 內),代碼邏輯比較簡潔。

class FirstPage extends StatelessWidget {
  final _model = FirstPageModel();
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Container(
          alignment: Alignment.center,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TosObWidget(() => Text(_model.textA.value)),
              TosObWidget(() => Text(_model.textB.value)),
              TextButton(
                onPressed: () => _model.textA.value = "你好",
                child: Text("按鈕"),
              ),
            ],
          ),
        ),
      );
}

model 實現:

model 的實現更加簡潔,不需要繼承 ChangeNotifier,所以可以把狀態數據定義在任何我們想要的地方,使用. tos 擴展屬性返回一個包含默認值的 RxObj 對象,當我們使用 set 方法更改 RxObj 的 value 的時候,通知依賴此對象的 TosObWidget 區域進行刷新,例:我們點擊按鈕的時候,_model.textA.value = "你好",執行後就會刷新依賴 textA 的 TosObWidget(() => Text(_model.textA.value)) 區域。

class FirstPageModel {
  final textA = "hello".tos;
  final textB = "world".tos;
}

查看刷新狀態(與 provider 對比):

oeqsvK

對比發現 TosObWidget 這種方式,只有依賴的數據發生變化的 TosObWidget 纔會更新狀態,可以實現狀態刷新粒度最小化,提高性能。

3.2 設計思路

3.2.1 TosObWidget

圖 2 狀態管理流程

首先是使用入口,定義一個 TosObWidget 控件,入參爲 build 函數,返回 widget,每個 TosObWidget 就是一個可獨立進行狀態刷新的區域,TosObWidget 控件的實現如下:

typedef WidgetCallback = Widget Function();
//TosObWidget的實現
class TosObWidget extends _ObzWidget {
  final WidgetCallback builder;
  const TosObWidget(this.builder, {Key key}) : super(key: key);
  @override
  Widget build() => builder();
}
//TosObWidget的父類,TosObWidget的build函數爲重載的其父類_ObzWidget的build函數,
//最終會被_ObzWidget的_ObzState調用
abstract class _ObzWidget extends StatefulWidget {
  const _ObzWidget({Key key}) : super(key: key);
  //創建狀態
  @override
  _ObzState createState() => _ObzState();
  //創建widget
  @protected
  Widget build();
}
//state的實現,主要邏輯都在這個類進行實現
class _ObzState extends State<_ObzWidget> {
  RxObserver _observer;
  ///構造函數
  _ObzState() {
    _observer = RxObserver();
  }
  ///初始化
  @override
  void initState() {
    _observer.observe(_updateUI);
    super.initState();
  }
  ///刷新UI
  void _updateUI() {
    if (mounted) {
      setState(() {});
    }
  }
  ///頁面銷燬
  @override
  void dispose() {
    _observer?.close();
    super.dispose();
  }
  ///創建widget,在這裏進行狀態觀察的綁定
  Widget get buildWidgets {
    //獲取proxy原來的值,也就是null
    final observer = RxObserver.proxy;
    //把widget的觀察者賦值過去
    RxObserver.proxy = _observer;
    //在widget.build()的時機進行綁定
    final widgets = widget.build();
    //綁定後恢復proxy的值,避免其他widget引用出現錯誤
    RxObserver.proxy = observer;
    return widgets;
  }
  @override
  Widget build(BuildContext context) => buildWidgets;
}

3.2.2 TosObWidget 邏輯分析

  1. 首先_ObzState 依賴一個 RxObserver _observer 變量

2.RxObserver _observer 這個 變量持有了_updateUI() 這個方法,最終會通過這個方法刷新 TosOBWidget 的狀態

  1. 當 TosObWidget 執行 build 的時候,會通過一個靜態變量 RxObserver.proxy 把_observer 共享出去

  2. 這樣 TosObWidget 包裹的內容,使用 RxObj 的 getValue 的時候會拿到被共享的_observer,這時建立 RxObj 和 TosObWidget 的聯繫

  3. 聯繫建立後,重置共享變量 RxObserver.proxy

  4. 這樣在 RxObj 的 value 執行 set 方法時,會調用到與其綁定的 TosObWidget 的_updateUI() 這個函數

3.2.3 RxObj 的實現

圖 3 RxObj 實現流程圖

RxObj 的代碼實現:

  1. 當執行 RxObj 的 value 的 get 方法時,代碼如下,拿到 RxObserver 的靜態成員變量 proxy,類型爲 RxObserver(即爲上一步 TosObWidget 共享出來的_observer)

  2. 判斷 RxObserver.proxy 不爲空,且沒有被添加到_observers 列表( List _observers),則添加

  3. 當執行 RxObj 的 value 的 set 方法時,校驗 value 是否與當前的 value 值相同,且判斷是否是首次創建(首次創建不會執行狀態刷新)

  4. 校驗完成後則賦值執行 refresh() 函數,更新 TosObWidget 的狀態

///RxObj類,所有數據類型可通過.obz擴展屬性獲得此示例
///當value發生變化時,通知RxObserver更新UI
class RxObj<T> {
  T _value;
  bool _firstRebuild = true;
  final List<RxObserver> _observers = [];
  RxObj(this._value);
  ///構造函數重載,如果沒有初始值的時候使用
  RxObj.obj();
  T get value {
    if (RxObserver.proxy != null && !_observers.contains(RxObserver.proxy)) {
      _observers.add(RxObserver.proxy);
    }
    return _value;
  }
  set value(T val) {
    if (_value == val && _firstRebuild) {
      return;
    }
    _firstRebuild = false;
    _value = val;
    //數據變化的時候,更新UI
    refresh();
  }
  ///刷新UI,使用引用數據類型的時候,如果沒有調用set方法,需要手動refresh()一下
  void refresh() {
    if (_observers.isNotEmpty) {
      for (var observer in _observers) {
        if (observer.canUpdate) {
//observer.update()函數即爲執行與Rxobj關聯的TosObWidget的_updateUI()函數
          observer.update();
        }
      }
    }
  }
}

看下 RxObserver 的實現:

/// 通過靜態變量proxy,在widget build的時候與狀態綁定
/// 定義一個觀察者,觀察RxObj<T>的數據變化,並通知UI更新
class RxObserver<T> {
  ///觀察數據變化方法回調
  VoidCallback update;
  ///判斷當前widget是否具備刷新能力(Obz)
  bool get canUpdate => update != null;
  ///TosObWidget dispose的時候執行關閉
  void close() {
    update = null;
  }
  ///注意:這是一個臨時變量,最用爲使RxObj和TosObWidget建立起訂閱關係
  static RxObserver proxy;
  ///觀察事件的變化
  observe(VoidCallback update) {
    this.update = update;
  }
}

至此整個實現流程已經貫通了,接下來看下如何使用:

  1. 通過. tos 擴展屬性定義 RxObj 變量:
class FirstPageModel {
  final textA = "hello".tos;
  final textB = "world".tos;
}

6.tos 擴展屬性的實現如下:

///RxObj擴展屬性
extension RxT<T> on T {
  ///返回RxObj實例,使用.tos
  RxObj<T> get tos => RxObj<T>(this);
}
  1. 如果要創建一個默認值爲空的,RxObj 實例,使用如下方式:
final emptyValue = RxObj<String>.obj();

此時如果我們使用 RxObj 的 setValue 方法,就會刷新依賴它的所有 TosObWidget 控件,如果有些情況下,沒有調用 setValue 方法,比如 RxObj 的 value 是一個 list,但是需要刷新狀態,可手動調用 refresh() 方法,實現如下:

final listValue = ["aaa", "bbb"].tos;
void add(String value) {
  listValue.value.add(value);
  listValue.refresh();
}

至此,就完成了 TosObWidget 控件的狀態刷新。

04 總結

注:基於本文示例的功能邏輯進行對比

eBnlG9

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oNjRt2qGHetEXLtkDPU63A