作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設(shè)計模式、flutter_redux 、 fish_redux 來全面深入分析, Flutter 中大家最為關(guān)心的狀態(tài)管理機(jī)制,理解各大框架中如何設(shè)計實現(xiàn)狀態(tài)管理,從而選出你最為合適的 state “大管家”。
Flutter 文章匯總地址:
Flutter 完整實戰(zhàn)實戰(zhàn)系列文章專欄
Flutter 番外的世界系列文章專欄
在所有 響應(yīng)式編程 中,狀態(tài)管理一直老生常談的話題,而在 Flutter 中,目前主流的有 scope_model 、BloC 設(shè)計模式 、flutter_redux 、fish_redux 等四種設(shè)計,它們的 復(fù)雜度 和 上手難度 是逐步遞增的,但同時 可拓展性 、解耦度 和 復(fù)用能力 也逐步提升。
基于前篇,我們對 Stream 已經(jīng)有了全面深入的理解,后面可以發(fā)現(xiàn)這四大框架或多或少都有 Stream 的應(yīng)用,不過還是那句老話,合適才是最重要,不要為了設(shè)計而設(shè)計 。
本文Demo源碼
GSYGithubFlutter 完整開源項目
一、scoped_model
scoped_model 是 Flutter 最為簡單的狀態(tài)管理框架,它充分利用了 Flutter 中的一些特性,只有一個 dart 文件的它,極簡的實現(xiàn)了一般場景下的狀態(tài)管理。
如下方代碼所示,利用 scoped_model 實現(xiàn)狀態(tài)管理只需要三步 :
- 定義
Model 的實現(xiàn),如 CountModel ,并且在狀態(tài)改變時執(zhí)行 notifyListeners() 方法。
- 使用
ScopedModel Widget 加載 Model 。
- 使用
ScopedModelDescendant 或者 ScopedModel.of<CountModel>(context) 加載 Model 內(nèi)狀態(tài)數(shù)據(jù)。
是不是很簡單?那僅僅一個 dart 文件,如何實現(xiàn)這樣的效果的呢?后面我們馬上開始剝析它。
class ScopedPage extends StatelessWidget {
final CountModel _model = new CountModel();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("scoped"),
),
body: Container(
child: new ScopedModel<CountModel>(
model: _model,
child: CountWidget(),
),
));
}
}
class CountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new ScopedModelDescendant<CountModel>(
builder: (context, child, model) {
return new Column(
children: <Widget>[
new Expanded(child: new Center(child: new Text(model.count.toString()))),
new Center(
child: new FlatButton(
onPressed: () {
model.add();
},
color: Colors.blue,
child: new Text("+")),
),
],
);
});
}
}
class CountModel extends Model {
static CountModel of(BuildContext context) =>
ScopedModel.of<CountModel>(context);
int _count = 0;
int get count => _count;
void add() {
_count++;
notifyListeners();
}
}
如下圖所示,在 scoped_model 的整個實現(xiàn)流程中,ScopedModel 這個 Widget 很巧妙的借助了 AnimatedBuildler 。
因為 AnimatedBuildler 繼承了 AnimatedWidget ,在 AnimatedWidget 的生命周期中會對 Listenable 接口添加監(jiān)聽,而 Model 恰好就實現(xiàn)了 Listenable 接口,整個流程總結(jié)起來就是:
Model 實現(xiàn)了 Listenable 接口,內(nèi)部維護(hù)一個 Set<VoidCallback> _listeners 。
- 當(dāng)
Model 設(shè)置給 AnimatedBuildler 時, Listenable 的 addListener 會被調(diào)用,然后添加一個 _handleChange 監(jiān)聽到 _listeners 這個 Set 中。
- 當(dāng)
Model 調(diào)用 notifyListeners 時,會通過異步方法 scheduleMicrotask 去從頭到尾執(zhí)行一遍 _listeners 中的 _handleChange 。
_handleChange 監(jiān)聽被調(diào)用,執(zhí)行了 setState({}) 。

整個流程是不是很巧妙,機(jī)制的利用了 AnimatedWidget 和 Listenable 在 Flutter 中的特性組合,至于 ScopedModelDescendant 就只是為了跨 Widget 共享 Model 而做的一層封裝,主要還是通過 ScopedModel.of<CountModel>(context) 獲取到對應(yīng) Model 對象,這這個實現(xiàn)上,scoped_model 依舊利用了 Flutter 的特性控件 InheritedWidget 實現(xiàn)。
InheritedWidget
在 scoped_model 中我們可以通過 ScopedModel.of<CountModel>(context) 獲取我們的 Model ,其中最主要是因為其內(nèi)部的 build 的時候,包裹了一個 _InheritedModel 控件,而它繼承了 InheritedWidget 。
為什么我們可以通過 context 去獲取到共享的 Model 對象呢?
首先我們知道 context 只是接口,而在 Flutter 中 context 的實現(xiàn)是 Element ,在 Element 的 inheritFromWidgetOfExactType 方法實現(xiàn)里,有一個 Map<Type, InheritedElement> _inheritedWidgets 的對象。
_inheritedWidgets 一般情況下是空的,只有當(dāng)父控件是 InheritedWidget 或者本身是 InheritedWidgets 時才會有被初始化,而當(dāng)父控件是 InheritedWidget 時,這個 Map 會被一級一級往下傳遞與合并 。
所以當(dāng)我們通過 context 調(diào)用 inheritFromWidgetOfExactType 時,就可以往上查找到父控件的 Widget,從在 scoped_model 獲取到 _InheritedModel 中的Model 。
二、BloC
BloC 全稱 Business Logic Component ,它屬于一種設(shè)計模式,在 Flutter 中它主要是通過 Stream 與 SteamBuilder 來實現(xiàn)設(shè)計的,所以 BloC 實現(xiàn)起來也相對簡單,關(guān)于 Stream 與 SteamBuilder 的實現(xiàn)原理可以查看前篇,這里主要展示如何完成一個簡單的 BloC 。
如下代碼所示,整個流程總結(jié)為:
- 定義一個
PageBloc 對象,利用 StreamController 創(chuàng)建 Sink 與 Stream 。
PageBloc 對外暴露 Stream 用來與 StreamBuilder 結(jié)合;暴露 add 方法提供外部調(diào)用,內(nèi)部通過 Sink 更新 Stream 。
- 利用
StreamBuilder 加載監(jiān)聽 Stream 數(shù)據(jù)流,通過 snapShot 中的 data 更新控件。
當(dāng)然,如果和 rxdart 結(jié)合可以簡化 StreamController 的一些操作,同時如果你需要利用 BloC 模式實現(xiàn)狀態(tài)共享,那么自己也可以封裝多一層 InheritedWidgets 的嵌套,如果對于這一塊有疑惑的話,推薦可以去看看上一篇的 Stream 解析。
class _BlocPageState extends State<BlocPage> {
final PageBloc _pageBloc = new PageBloc();
@override
void dispose() {
_pageBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: new StreamBuilder(
initialData: 0,
stream: _pageBloc.stream,
builder: (context, snapShot) {
return new Column(
children: <Widget>[
new Expanded(
child: new Center(
child: new Text(snapShot.data.toString()))),
new Center(
child: new FlatButton(
onPressed: () {
_pageBloc.add();
},
color: Colors.blue,
child: new Text("+")),
),
new SizedBox(
height: 100,
)
],
);
}),
),
);
}
}
class PageBloc {
int _count = 0;
///StreamController
StreamController<int> _countController = StreamController<int>();
///對外提供入口
StreamSink<int> get _countSink => _countController.sink;
///提供 stream StreamBuilder 訂閱
Stream<int> get stream => _countController.stream;
void dispose() {
_countController.close();
}
void add() {
_count++;
_countSink.add(_count);
}
}
三、flutter_redux
相信如果是前端開發(fā)者,對于 redux 模式并不會陌生,而 flutter_redux 可以看做是利用了 Stream 特性的 scope_model 升級版,通過 redux 設(shè)計模式來完成解耦和拓展。
當(dāng)然,更多的功能和更好的拓展性,也造成了代碼的復(fù)雜度和上手難度 ,因為 flutter_redux 的代碼使用篇幅問題,這里就不展示所有代碼了,需要看使用代碼的可直接從 demo 獲取,現(xiàn)在我們直接看 flutter_redux 是如何實現(xiàn)狀態(tài)管理的吧。

如上圖,我們知道 redux 中一般有 Store 、 Action 、 Reducer 三個主要對象,之外還有 Middleware 中間件用于攔截,所以如下代碼所示:
- 創(chuàng)建
Store 用于管理狀態(tài) 。
- 給
Store 增加 appReducer 合集方法,增加需要攔截的 middleware ,并初始化狀態(tài)。
- 將
Store 設(shè)置給 StoreProvider 這個 InheritedWidget 。
- 通過
StoreConnector / StoreBuilder 加載顯示 Store 中的數(shù)據(jù)。
之后我們可以 dispatch 一個 Action ,在經(jīng)過 middleware 之后,觸發(fā)對應(yīng)的 Reducer 返回數(shù)據(jù),而事實上這里核心的內(nèi)容實現(xiàn),還是 Stream 和 StreamBuilder 的結(jié)合使用 ,接下來就讓我們看看這個流程是如何聯(lián)動起來的吧。
class _ReduxPageState extends State<ReduxPage> {
///初始化store
final store = new Store<CountState>(
/// reducer 合集方法
appReducer,
///中間鍵
middleware: middleware,
///初始化狀態(tài)
initialState: new CountState(count: 0),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("redux"),
),
body: Container(
/// StoreProvider InheritedWidget
/// 加載 store 共享
child: new StoreProvider(
store: store,
child: CountWidget(),
),
));
}
}
如下圖所示,是 flutter_redux 從入口到更新的完整流程圖,整理這個流程其中最關(guān)鍵有幾個點(diǎn)是:
StoreProvider 是 InheritedWidgets ,所以它可以通過 context 實現(xiàn)狀態(tài)共享。
StreamBuilder / StoreConnector 的內(nèi)部實現(xiàn)主要是 StreamBuilder 。
Store 內(nèi)部是通過 StreamController.broadcast 創(chuàng)建的 Stream ,然后在 StoreConnector 中通過 Stream 的 map 、transform 實現(xiàn)小狀態(tài)的變換,最后更新到 StreamBuilder 。
那么現(xiàn)在看下圖流程有點(diǎn)暈?下面我們直接分析圖中流程。

可以看出整個流程的核心還是 Stream ,基于這幾個關(guān)鍵點(diǎn),我們把上圖的流程整理為:
- 1、
Store 創(chuàng)建時傳入 reducer 對象和 middleware 數(shù)組,同時通過 StreamController.broadcast 創(chuàng)建了 _changeController 對象。
- 2、
Store 利用 middleware 和 _changeController 組成了一個 NextDispatcher 方法數(shù)組 ,并把 _changeController 所在的 NextDispatcher 方法放置在數(shù)組最后位置。
- 3、
StoreConnector 內(nèi)通過 Store 的 _changeController 獲取 Stream ,并進(jìn)行了一系列變換后,最終 Stream 設(shè)置給了 StreamBuilder 。
- 4、當(dāng)我們調(diào)用
Stroe 的 dispatch 方法時,我們會先進(jìn)過 NextDispatcher 數(shù)組中的一系列 middleware 攔截器,最終調(diào)用到隊末的 _changeController 所在的 NextDispatcher 。
- 5、最后一個
NextDispatcher 執(zhí)行時會先執(zhí)行 reducer 方法獲取新的 state ,然后通過 _changeController.add 將狀態(tài)加載到 Stream 流程中,觸發(fā) StoreConnector 的 StreamBuilder 更新數(shù)據(jù)。
如果對于 Stream 流程不熟悉的還請看上篇。
現(xiàn)在再對照流程圖會不會清晰很多了?
在 flutter_redux 中,開發(fā)者的每個操作都只是一個 Action ,而這個行為所觸發(fā)的邏輯完全由 middleware 和 reducer 決定,這樣的設(shè)計在一定程度上將業(yè)務(wù)與UI隔離,同時也統(tǒng)一了狀態(tài)的管理。
比如你一個點(diǎn)擊行為只是發(fā)出一個 RefrshAction ,但是通過 middleware 攔截之后,在其中異步處理完幾個數(shù)據(jù)接口,然后重新 dispatch 出 Action1 、Action2 、Action3 去更新其他頁面, 類似的 redux_epics 庫就是這樣實現(xiàn)異步的 middleware 邏輯。
四、fish_redux
如果說 flutter_redux 屬于相對復(fù)雜的狀態(tài)管理設(shè)置的話,那么閑魚開源的 fish_redux 可謂 “不走尋常路” 了,雖然是基于 redux 原有的設(shè)計理念,同時也有使用到 Stream ,但是相比較起來整個設(shè)計完全是 超脫三界,如果是前面的都是簡單的拼積木,那是 fish_redux 就是積木界的樂高。

因為篇幅原因,這里也只展示部分代碼,其中 reducer 還是我們熟悉的存在,而閑魚在這 redux 的基礎(chǔ)上提出了 Comoponent 的概念,這個概念下 fish_redux 是從 Context 、Widget 等地方就開始全面“入侵”你的代碼,從而帶來“超級賽亞人”版的 redux 。
如下代碼所示,默認(rèn)情況我們需要:
- 繼承
Page 實現(xiàn)我們的頁面。
- 定義好我們的
State 狀態(tài)。
- 定義
effect 、 middleware 、reducer 用于實現(xiàn)副作用、中間件、結(jié)果返回處理。
- 定義
view 用于繪制頁面。
- 定義
dependencies 用戶裝配控件,這里最騷氣的莫過于重載了 + 操作符,然后利用 Connector 從 State 挑選出數(shù)據(jù),然后通過 Component 繪制。
現(xiàn)在看起來使用流程是不是變得復(fù)雜了?
但是這帶來的好處就是 復(fù)用的顆粒度更細(xì)了,裝配和功能更加的清晰。 那這個過程是如何實現(xiàn)的呢?后面我們將分析這個復(fù)雜的流程。
class FishPage extends Page<CountState, Map<String, dynamic>> {
FishPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
///配置 View 顯示
view: buildView,
///配置 Dependencies 顯示
dependencies: Dependencies<CountState>(
slots: <String, Dependent<CountState>>{
///通過 Connector() 從 大 state 轉(zhuǎn)化處小 state
///然后將數(shù)據(jù)渲染到 Component
'count-double': DoubleCountConnector() + DoubleCountComponent()
}
),
middleware: <Middleware<CountState>>[
///中間鍵打印log
logMiddleware(tag: 'FishPage'),
]
);
}
///渲染主頁
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: new Text("fish"),
),
body: new Column(
children: <Widget>[
///viewService 渲染 dependencies
viewService.buildComponent('count-double'),
new Expanded(child: new Center(child: new Text(state.count.toString()))),
new Center(
child: new FlatButton(
onPressed: () {
///+
dispatch(CountActionCreator.onAddAction());
},
color: Colors.blue,
child: new Text("+")),
),
new SizedBox(
height: 100,
)
],
));
}
如下大圖所示,整個聯(lián)動的流程比 flutter_redux 復(fù)雜了更多( 如果看不清可以點(diǎn)擊大圖 ),而這個過程我們總結(jié)起來就是:
- 1、
Page 的構(gòu)建需要 State 、Effect 、Reducer 、view 、dependencies 、 middleware 等參數(shù)。
- 2、
Page 的內(nèi)部 PageProvider 是一個 InheritedWidget 用戶狀態(tài)共享。
- 3、
Page 內(nèi)部會通過 createMixedStore 創(chuàng)建 Store 對象。
- 4、
Store 對象對外提供的 subscribe 方法,在訂閱時會將訂閱的方法添加到內(nèi)部 List<_VoidCallback> _listeners 。
- 5、
Store 對象內(nèi)部的 StreamController.broadcast 創(chuàng)建出了 _notifyController 對象用于廣播更新。
- 6、
Store 對象內(nèi)部的 subscribe 方法,會在 ComponentState 中添加訂閱方法 onNotify ,如果調(diào)用在 onNotify 中最終會執(zhí)行 setState 更新UI。
- 7、
Store 對象對外提供的 dispatch 方法,執(zhí)行時內(nèi)部會執(zhí)行 4 中的 List<_VoidCallback> _listeners ,觸發(fā) onNotify 。
- 8、
Page 內(nèi)部會通過 Logic 創(chuàng)建 Dispatch ,執(zhí)行時經(jīng)歷 Effect -> Middleware -> Stroe.dispatch -> Reducer -> State -> _notifyController -> _notifyController.add(state) 等流程。
- 9、以上流程最終就是
Dispatch 觸發(fā) Store 內(nèi)部 _notifyController , 最終會觸發(fā) ComponentState 中的 onNotify 中的setState 更新UI

是不是有很多對象很陌生?
確實 fish_redux 的整體流程更加復(fù)雜,內(nèi)部的 ContxtSys 、Componet 、ViewSerivce 、 Logic 等等概念設(shè)計,這里因為篇幅有限就不詳細(xì)拆分展示了,但從整個流程可以看出 fish_redux 從控件到頁面更新,全都進(jìn)行了新的獨(dú)立設(shè)計,而這里面最有意思的,莫不過 dependencies 。
如下圖所示,得益于fish_redux 內(nèi)部 ConnOpMixin 中對操作符的重載,我們可以通過 DoubleCountConnector() + DoubleCountComponent() 來實現(xiàn)Dependent 的組裝。

Dependent 的組裝中 Connector 會從總 State 中讀取需要的小 State 用于 Component 的繪制,這樣很好的達(dá)到了 模塊解耦與復(fù)用 的效果。
而使用中我們組裝的 dependencies 最后都會通過 ViewService 提供調(diào)用調(diào)用能力,比如調(diào)用 buildAdapter 用于列表能力,調(diào)用 buildComponent 提供獨(dú)立控件能力等。
可以看出 flutter_redux 的內(nèi)部實現(xiàn)復(fù)雜度是比較高的,在提供組裝、復(fù)用、解耦的同時,也對項目進(jìn)行了一定程度的入侵,這里的篇幅可能不能很全面的分析 flutter_redux 中的整個流程,但是也能讓你理解整個流程的關(guān)鍵點(diǎn),細(xì)細(xì)品味設(shè)計之美。
自此,第十二篇終于結(jié)束了!(///▽///)
資源推薦
- Github : https://github.com/CarGuo
- 本文Demo :https://github.com/CarGuo/state_manager_demo
- 本文代碼 :https://github.com/CarGuo/GSYGithubAppFlutter
完整開源項目推薦:
Flutter 文章匯總地址:
Flutter 完整實戰(zhàn)實戰(zhàn)系列文章專欄
Flutter 番外的世界系列文章專欄
|