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 控件刷新
2.2.2 問題分析
-
使用不太靈活,想要消費事件刷新 UI 必須有頂層的 Provider 提供 model,在一些複雜場景可能會增加邏輯複雜度
-
狀態刷新,不能實現最小粒度的管理
-
代碼不夠簡潔
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 對比):
對比發現 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 邏輯分析
- 首先_ObzState 依賴一個 RxObserver _observer 變量
2.RxObserver _observer 這個 變量持有了_updateUI() 這個方法,最終會通過這個方法刷新 TosOBWidget 的狀態
-
當 TosObWidget 執行 build 的時候,會通過一個靜態變量 RxObserver.proxy 把_observer 共享出去
-
這樣 TosObWidget 包裹的內容,使用 RxObj 的 getValue 的時候會拿到被共享的_observer,這時建立 RxObj 和 TosObWidget 的聯繫
-
聯繫建立後,重置共享變量 RxObserver.proxy
-
這樣在 RxObj 的 value 執行 set 方法時,會調用到與其綁定的 TosObWidget 的_updateUI() 這個函數
3.2.3 RxObj 的實現
圖 3 RxObj 實現流程圖
RxObj 的代碼實現:
-
當執行 RxObj 的 value 的 get 方法時,代碼如下,拿到 RxObserver 的靜態成員變量 proxy,類型爲 RxObserver(即爲上一步 TosObWidget 共享出來的_observer)
-
判斷 RxObserver.proxy 不爲空,且沒有被添加到_observers 列表( List _observers),則添加
-
當執行 RxObj 的 value 的 set 方法時,校驗 value 是否與當前的 value 值相同,且判斷是否是首次創建(首次創建不會執行狀態刷新)
-
校驗完成後則賦值執行 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;
}
}
至此整個實現流程已經貫通了,接下來看下如何使用:
- 通過. 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);
}
- 如果要創建一個默認值爲空的,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 總結
注:基於本文示例的功能邏輯進行對比
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/oNjRt2qGHetEXLtkDPU63A