作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設(shè)計(jì)模式、flutter_redux 、 fish_redux 來全面深入分析, Flutter 中大家最為關(guān)心的狀態(tài)管理機(jī)制,理解各大框架中如何設(shè)計(jì)實(shí)現(xiàn)狀態(tài)管理,從而選出你最為合適的 state “大管家”。
文章匯總地址:
在所有 響應(yīng)式編程 中,狀態(tài)管理一直老生常談的話題,而在 Flutter 中,目前主流的有 scope_model
、BloC 設(shè)計(jì)模式
、flutter_redux
、fish_redux
等四種設(shè)計(jì),它們的 復(fù)雜度 和 上手難度 是逐步遞增的,但同時(shí) 可拓展性 、解耦度 和 復(fù)用能力 也逐步提升。
基于前篇,我們對 Stream
已經(jīng)有了全面深入的理解,后面可以發(fā)現(xiàn)這四大框架或多或少都有 Stream
的應(yīng)用,不過還是那句老話,合適才是最重要,不要為了設(shè)計(jì)而設(shè)計(jì) 。
一、scoped_model
scoped_model
是 Flutter 最為簡單的狀態(tài)管理框架,它充分利用了 Flutter 中的一些特性,只有一個(gè) dart 文件的它,極簡的實(shí)現(xiàn)了一般場景下的狀態(tài)管理。
如下方代碼所示,利用 scoped_model
實(shí)現(xiàn)狀態(tài)管理只需要三步 :
- 定義
Model
的實(shí)現(xiàn),如CountModel
,并且在狀態(tài)改變時(shí)執(zhí)行notifyListeners()
方法。 - 使用
ScopedModel
Widget 加載Model
。 - 使用
ScopedModelDescendant
或者ScopedModel.of<CountModel>(context)
加載Model
內(nèi)狀態(tài)數(shù)據(jù)。
是不是很簡單?那僅僅一個(gè) dart 文件,如何實(shí)現(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
的整個(gè)實(shí)現(xiàn)流程中,ScopedModel
這個(gè) Widget 很巧妙的借助了 AnimatedBuildler
。
因?yàn)? AnimatedBuildler
繼承了 AnimatedWidget
,在 AnimatedWidget
的生命周期中會(huì)對 Listenable
接口添加監(jiān)聽,而 Model
恰好就實(shí)現(xiàn)了 Listenable
接口,整個(gè)流程總結(jié)起來就是:
-
Model
實(shí)現(xiàn)了Listenable
接口,內(nèi)部維護(hù)一個(gè)Set<VoidCallback> _listeners
。 - 當(dāng)
Model
設(shè)置給AnimatedBuildler
時(shí),Listenable
的addListener
會(huì)被調(diào)用,然后添加一個(gè)_handleChange
監(jiān)聽到_listeners
這個(gè) Set 中。 - 當(dāng)
Model
調(diào)用notifyListeners
時(shí),會(huì)通過異步方法scheduleMicrotask
去從頭到尾執(zhí)行一遍_listeners
中的_handleChange
。 -
_handleChange
監(jiān)聽被調(diào)用,執(zhí)行了setState({})
。
整個(gè)流程是不是很巧妙,機(jī)制的利用了 AnimatedWidget
和 Listenable
在 Flutter 中的特性組合,至于 ScopedModelDescendant
就只是為了跨 Widget 共享 Model
而做的一層封裝,主要還是通過 ScopedModel.of<CountModel>(context)
獲取到對應(yīng) Model 對象,這這個(gè)實(shí)現(xiàn)上,scoped_model
依舊利用了 Flutter 的特性控件 InheritedWidget
實(shí)現(xiàn)。
InheritedWidget
在 scoped_model
中我們可以通過 ScopedModel.of<CountModel>(context)
獲取我們的 Model ,其中最主要是因?yàn)槠鋬?nèi)部的 build 的時(shí)候,包裹了一個(gè) _InheritedModel
控件,而它繼承了 InheritedWidget
。
為什么我們可以通過 context
去獲取到共享的 Model
對象呢?
首先我們知道 context
只是接口,而在 Flutter 中 context
的實(shí)現(xiàn)是 Element
,在 Element
的 inheritFromWidgetOfExactType
方法實(shí)現(xiàn)里,有一個(gè) Map<Type, InheritedElement> _inheritedWidgets
的對象。
_inheritedWidgets
一般情況下是空的,只有當(dāng)父控件是 InheritedWidget
或者本身是 InheritedWidgets
時(shí)才會(huì)有被初始化,而當(dāng)父控件是 InheritedWidget
時(shí),這個(gè) Map 會(huì)被一級一級往下傳遞與合并 。
所以當(dāng)我們通過 context
調(diào)用 inheritFromWidgetOfExactType
時(shí),就可以往上查找到父控件的 Widget,從在 scoped_model
獲取到 _InheritedModel
中的Model
。
二、BloC
BloC
全稱 Business Logic Component ,它屬于一種設(shè)計(jì)模式,在 Flutter 中它主要是通過 Stream
與 SteamBuilder
來實(shí)現(xiàn)設(shè)計(jì)的,所以 BloC
實(shí)現(xiàn)起來也相對簡單,關(guān)于 Stream
與 SteamBuilder
的實(shí)現(xiàn)原理可以查看前篇,這里主要展示如何完成一個(gè)簡單的 BloC
。
如下代碼所示,整個(gè)流程總結(jié)為:
- 定義一個(gè)
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
的一些操作,同時(shí)如果你需要利用 BloC
模式實(shí)現(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
模式并不會(huì)陌生,而 flutter_redux
可以看做是利用了 Stream
特性的 scope_model
升級版,通過 redux
設(shè)計(jì)模式來完成解耦和拓展。
當(dāng)然,更多的功能和更好的拓展性,也造成了代碼的復(fù)雜度和上手難度 ,因?yàn)? flutter_redux
的代碼使用篇幅問題,這里就不展示所有代碼了,需要看使用代碼的可直接從 demo 獲取,現(xiàn)在我們直接看 flutter_redux
是如何實(shí)現(xiàn)狀態(tài)管理的吧。
如上圖,我們知道 redux
中一般有 Store
、 Action
、 Reducer
三個(gè)主要對象,之外還有 Middleware
中間件用于攔截,所以如下代碼所示:
- 創(chuàng)建
Store
用于管理狀態(tài) 。 - 給
Store
增加appReducer
合集方法,增加需要攔截的middleware
,并初始化狀態(tài)。 - 將
Store
設(shè)置給StoreProvider
這個(gè)InheritedWidget
。 - 通過
StoreConnector
/StoreBuilder
加載顯示Store
中的數(shù)據(jù)。
之后我們可以 dispatch
一個(gè) Action ,在經(jīng)過 middleware
之后,觸發(fā)對應(yīng)的 Reducer 返回?cái)?shù)據(jù),而事實(shí)上這里核心的內(nèi)容實(shí)現(xiàn),還是 Stream
和 StreamBuilder
的結(jié)合使用 ,接下來就讓我們看看這個(gè)流程是如何聯(lián)動(dòng)起來的吧。
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
從入口到更新的完整流程圖,整理這個(gè)流程其中最關(guān)鍵有幾個(gè)點(diǎn)是:
-
StoreProvider
是InheritedWidgets
,所以它可以通過context
實(shí)現(xiàn)狀態(tài)共享。 -
StreamBuilder
/StoreConnector
的內(nèi)部實(shí)現(xiàn)主要是StreamBuilder
。 -
Store
內(nèi)部是通過StreamController.broadcast
創(chuàng)建的Stream
,然后在StoreConnector
中通過Stream
的map
、transform
實(shí)現(xiàn)小狀態(tài)的變換,最后更新到StreamBuilder
。
那么現(xiàn)在看下圖流程有點(diǎn)暈?下面我們直接分析圖中流程。
可以看出整個(gè)流程的核心還是 Stream
,基于這幾個(gè)關(guān)鍵點(diǎn),我們把上圖的流程整理為:
- 1、
Store
創(chuàng)建時(shí)傳入reducer
對象和middleware
數(shù)組,同時(shí)通過StreamController.broadcast
創(chuàng)建了_changeController
對象。 - 2、
Store
利用middleware
和_changeController
組成了一個(gè)NextDispatcher
方法數(shù)組 ,并把_changeController
所在的NextDispatcher
方法放置在數(shù)組最后位置。 - 3、
StoreConnector
內(nèi)通過Store
的_changeController
獲取Stream
,并進(jìn)行了一系列變換后,最終Stream
設(shè)置給了StreamBuilder
。 - 4、當(dāng)我們調(diào)用
Stroe
的dispatch
方法時(shí),我們會(huì)先進(jìn)過NextDispatcher
數(shù)組中的一系列middleware
攔截器,最終調(diào)用到隊(duì)末的_changeController
所在的NextDispatcher
。 - 5、最后一個(gè)
NextDispatcher
執(zhí)行時(shí)會(huì)先執(zhí)行reducer
方法獲取新的state
,然后通過_changeController.add
將狀態(tài)加載到Stream
流程中,觸發(fā)StoreConnector
的StreamBuilder
更新數(shù)據(jù)。
如果對于
Stream
流程不熟悉的還請看上篇。
現(xiàn)在再對照流程圖會(huì)不會(huì)清晰很多了?
在 flutter_redux
中,開發(fā)者的每個(gè)操作都只是一個(gè) Action
,而這個(gè)行為所觸發(fā)的邏輯完全由 middleware
和 reducer
決定,這樣的設(shè)計(jì)在一定程度上將業(yè)務(wù)與UI隔離,同時(shí)也統(tǒng)一了狀態(tài)的管理。
比如你一個(gè)點(diǎn)擊行為只是發(fā)出一個(gè)
RefrshAction
,但是通過middleware
攔截之后,在其中異步處理完幾個(gè)數(shù)據(jù)接口,然后重新dispatch
出Action1
、Action2
、Action3
去更新其他頁面, 類似的redux_epics
庫就是這樣實(shí)現(xiàn)異步的middleware
邏輯。
四、fish_redux
如果說 flutter_redux
屬于相對復(fù)雜的狀態(tài)管理設(shè)置的話,那么閑魚開源的 fish_redux
可謂 “不走尋常路” 了,雖然是基于 redux
原有的設(shè)計(jì)理念,同時(shí)也有使用到 Stream
,但是相比較起來整個(gè)設(shè)計(jì)完全是 超脫三界,如果是前面的都是簡單的拼積木,那是 fish_redux
就是積木界的樂高。
因?yàn)槠颍@里也只展示部分代碼,其中 reducer
還是我們熟悉的存在,而閑魚在這 redux
的基礎(chǔ)上提出了 Comoponent
的概念,這個(gè)概念下 fish_redux
是從 Context
、Widget
等地方就開始全面“入侵”你的代碼,從而帶來“超級賽亞人”版的 redux
。
如下代碼所示,默認(rèn)情況我們需要:
- 繼承
Page
實(shí)現(xiàn)我們的頁面。 - 定義好我們的
State
狀態(tài)。 - 定義
effect
、middleware
、reducer
用于實(shí)現(xiàn)副作用、中間件、結(jié)果返回處理。 - 定義
view
用于繪制頁面。 - 定義
dependencies
用戶裝配控件,這里最騷氣的莫過于重載了 + 操作符,然后利用Connector
從State
挑選出數(shù)據(jù),然后通過Component
繪制。
現(xiàn)在看起來使用流程是不是變得復(fù)雜了?
但是這帶來的好處就是 復(fù)用的顆粒度更細(xì)了,裝配和功能更加的清晰。 那這個(gè)過程是如何實(shí)現(xiàn)的呢?后面我們將分析這個(gè)復(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,
)
],
));
}
如下大圖所示,整個(gè)聯(lián)動(dòng)的流程比 flutter_redux
復(fù)雜了更多( 如果看不清可以點(diǎn)擊大圖 ),而這個(gè)過程我們總結(jié)起來就是:
1、
Page
的構(gòu)建需要State
、Effect
、Reducer
、view
、dependencies
、middleware
等參數(shù)。2、
Page
的內(nèi)部PageProvider
是一個(gè)InheritedWidget
用戶狀態(tài)共享。3、
Page
內(nèi)部會(huì)通過createMixedStore
創(chuàng)建Store
對象。4、
Store
對象對外提供的subscribe
方法,在訂閱時(shí)會(huì)將訂閱的方法添加到內(nèi)部List<_VoidCallback> _listeners
。5、
Store
對象內(nèi)部的StreamController.broadcast
創(chuàng)建出了_notifyController
對象用于廣播更新。6、
Store
對象內(nèi)部的subscribe
方法,會(huì)在ComponentState
中添加訂閱方法onNotify
,如果調(diào)用在onNotify
中最終會(huì)執(zhí)行setState
更新UI。7、
Store
對象對外提供的dispatch
方法,執(zhí)行時(shí)內(nèi)部會(huì)執(zhí)行 4 中的List<_VoidCallback> _listeners
,觸發(fā)onNotify
。8、
Page
內(nèi)部會(huì)通過Logic
創(chuàng)建Dispatch
,執(zhí)行時(shí)經(jīng)歷Effect
->Middleware
->Stroe.dispatch
->Reducer
->State
->_notifyController
->_notifyController.add(state)
等流程。9、以上流程最終就是
Dispatch
觸發(fā)Store
內(nèi)部_notifyController
, 最終會(huì)觸發(fā)ComponentState
中的onNotify
中的setState
更新UI
是不是有很多對象很陌生?
確實(shí) fish_redux
的整體流程更加復(fù)雜,內(nèi)部的 ContxtSys
、Componet
、ViewSerivce
、 Logic
等等概念設(shè)計(jì),這里因?yàn)槠邢蘧筒辉敿?xì)拆分展示了,但從整個(gè)流程可以看出 fish_redux
從控件到頁面更新,全都進(jìn)行了新的獨(dú)立設(shè)計(jì),而這里面最有意思的,莫不過 dependencies
。
如下圖所示,得益于fish_redux
內(nèi)部 ConnOpMixin
中對操作符的重載,我們可以通過 DoubleCountConnector() + DoubleCountComponent()
來實(shí)現(xiàn)Dependent
的組裝。
Dependent
的組裝中 Connector
會(huì)從總 State 中讀取需要的小 State 用于 Component
的繪制,這樣很好的達(dá)到了 模塊解耦與復(fù)用 的效果。
而使用中我們組裝的 dependencies
最后都會(huì)通過 ViewService
提供調(diào)用調(diào)用能力,比如調(diào)用 buildAdapter
用于列表能力,調(diào)用 buildComponent
提供獨(dú)立控件能力等。
可以看出 flutter_redux
的內(nèi)部實(shí)現(xiàn)復(fù)雜度是比較高的,在提供組裝、復(fù)用、解耦的同時(shí),也對項(xiàng)目進(jìn)行了一定程度的入侵,這里的篇幅可能不能很全面的分析 flutter_redux
中的整個(gè)流程,但是也能讓你理解整個(gè)流程的關(guān)鍵點(diǎn),細(xì)細(xì)品味設(shè)計(jì)之美。
自此,第十二篇終于結(jié)束了!(///▽///)
資源推薦
- 本文Demo :https://github.com/CarGuo/state_manager_demo
- Github : https://github.com/CarGuo/
- 開源 Flutter 完整項(xiàng)目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學(xué)習(xí)型項(xiàng)目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實(shí)戰(zhàn)電子書項(xiàng)目:https://github.com/CarGuo/GSYFlutterBook