作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設計模式、flutter_redux 、 fish_redux 來全面深入分析, Flutter 中大家最為關心的狀態管理機制,理解各大框架中如何設計實現狀態管理,從而選出你最為合適的 state “大管家”。
文章彙總地址:
在所有 響應式程式設計 中,狀態管理一直老生常談的話題,而在 Flutter 中,目前主流的有 scope_model
、BloC 設計模式
、flutter_redux
、fish_redux
等四種設計,它們的 複雜度 和 上手難度 是逐步遞增的,但同時 可擴充性 、解耦度 和 複用能力 也逐步提升。
基於前篇,我們對 Stream
已經有了全面深入的理解,後面可以發現這四大框架或多或少都有 Stream
的應用,不過還是那句老話,合適才是最重要,不要為了設計而設計 。
一、scoped_model
scoped_model
是 Flutter 最為簡單的狀態管理框架,它充分利用了 Flutter 中的一些特性,只有一個 dart 檔案的它,極簡的實現了一般場景下的狀態管理。
如下方程式碼所示,利用 scoped_model
實現狀態管理只需要三步 :
- 定義
Model
的實現,如CountModel
,並且在狀態改變時執行notifyListeners()
方法。 - 使用
ScopedModel
Widget 載入Model
。 - 使用
ScopedModelDescendant
或者ScopedModel.of<CountModel>(context)
載入Model
內狀態資料。
是不是很簡單?那僅僅一個 dart 檔案,如何實現這樣的效果的呢?後面我們馬上開始剝析它。
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
的整個實現流程中,ScopedModel
這個 Widget 很巧妙的藉助了 AnimatedBuildler
。
因為 AnimatedBuildler
繼承了 AnimatedWidget
,在 AnimatedWidget
的生命週期中會對 Listenable
介面新增監聽,而 Model
恰好就實現了 Listenable
介面,整個流程總結起來就是:
Model
實現了Listenable
介面,內部維護一個Set<VoidCallback> _listeners
。- 當
Model
設定給AnimatedBuildler
時,Listenable
的addListener
會被呼叫,然後新增一個_handleChange
監聽到_listeners
這個 Set 中。 - 當
Model
呼叫notifyListeners
時,會通過非同步方法scheduleMicrotask
去從頭到尾執行一遍_listeners
中的_handleChange
。 _handleChange
監聽被呼叫,執行了setState({})
。
整個流程是不是很巧妙,機制的利用了 AnimatedWidget
和 Listenable
在 Flutter 中的特性組合,至於 ScopedModelDescendant
就只是為了跨 Widget 共享 Model
而做的一層封裝,主要還是通過 ScopedModel.of<CountModel>(context)
獲取到對應 Model 物件,這這個實現上,scoped_model
依舊利用了 Flutter 的特性控制元件 InheritedWidget
實現。
InheritedWidget
在 scoped_model
中我們可以通過 ScopedModel.of<CountModel>(context)
獲取我們的 Model ,其中最主要是因為其內部的 build 的時候,包裹了一個 _InheritedModel
控制元件,而它繼承了 InheritedWidget
。
為什麼我們可以通過 context
去獲取到共享的 Model
物件呢?
首先我們知道 context
只是介面,而在 Flutter 中 context
的實現是 Element
,在 Element
的 inheritFromWidgetOfExactType
方法實現裡,有一個 Map<Type, InheritedElement> _inheritedWidgets
的物件。
_inheritedWidgets
一般情況下是空的,只有當父控制元件是 InheritedWidget
或者本身是 InheritedWidgets
時才會有被初始化,而當父控制元件是 InheritedWidget
時,這個 Map 會被一級一級往下傳遞與合併 。
所以當我們通過 context
呼叫 inheritFromWidgetOfExactType
時,就可以往上查詢到父控制元件的 Widget,從在 scoped_model
獲取到 _InheritedModel
中的Model
。
二、BloC
BloC
全稱 Business Logic Component ,它屬於一種設計模式,在 Flutter 中它主要是通過 Stream
與 SteamBuilder
來實現設計的,所以 BloC
實現起來也相對簡單,關於 Stream
與 SteamBuilder
的實現原理可以檢視前篇,這裡主要展示如何完成一個簡單的 BloC
。
如下程式碼所示,整個流程總結為:
- 定義一個
PageBloc
物件,利用StreamController
建立Sink
與Stream
。 PageBloc
對外暴露Stream
用來與StreamBuilder
結合;暴露 add 方法提供外部呼叫,內部通過Sink
更新Stream
。- 利用
StreamBuilder
載入監聽Stream
資料流,通過 snapShot 中的 data 更新控制元件。
當然,如果和 rxdart
結合可以簡化 StreamController
的一些操作,同時如果你需要利用 BloC
模式實現狀態共享,那麼自己也可以封裝多一層 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
相信如果是前端開發者,對於 redux
模式並不會陌生,而 flutter_redux
可以看做是利用了 Stream
特性的 scope_model
升級版,通過 redux
設計模式來完成解耦和擴充。
當然,更多的功能和更好的擴充性,也造成了程式碼的複雜度和上手難度 ,因為 flutter_redux
的程式碼使用篇幅問題,這裡就不展示所有程式碼了,需要看使用程式碼的可直接從 demo 獲取,現在我們直接看 flutter_redux
是如何實現狀態管理的吧。
如上圖,我們知道 redux
中一般有 Store
、 Action
、 Reducer
三個主要物件,之外還有 Middleware
中介軟體用於攔截,所以如下程式碼所示:
- 建立
Store
用於管理狀態 。 - 給
Store
增加appReducer
合集方法,增加需要攔截的middleware
,並初始化狀態。 - 將
Store
設定給StoreProvider
這個InheritedWidget
。 - 通過
StoreConnector
/StoreBuilder
載入顯示Store
中的資料。
之後我們可以 dispatch
一個 Action ,在經過 middleware
之後,觸發對應的 Reducer 返回資料,而事實上這裡核心的內容實現,還是 Stream
和 StreamBuilder
的結合使用 ,接下來就讓我們看看這個流程是如何聯動起來的吧。
class _ReduxPageState extends State<ReduxPage> {
///初始化store
final store = new Store<CountState>(
/// reducer 合集方法
appReducer,
///中間鍵
middleware: middleware,
///初始化狀態
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
從入口到更新的完整流程圖,整理這個流程其中最關鍵有幾個點是:
StoreProvider
是InheritedWidgets
,所以它可以通過context
實現狀態共享。StreamBuilder
/StoreConnector
的內部實現主要是StreamBuilder
。Store
內部是通過StreamController.broadcast
建立的Stream
,然後在StoreConnector
中通過Stream
的map
、transform
實現小狀態的變換,最後更新到StreamBuilder
。
那麼現在看下圖流程有點暈?下面我們直接分析圖中流程。
可以看出整個流程的核心還是 Stream
,基於這幾個關鍵點,我們把上圖的流程整理為:
- 1、
Store
建立時傳入reducer
物件和middleware
陣列,同時通過StreamController.broadcast
建立了_changeController
物件。 - 2、
Store
利用middleware
和_changeController
組成了一個NextDispatcher
方法陣列 ,並把_changeController
所在的NextDispatcher
方法放置在陣列最後位置。 - 3、
StoreConnector
內通過Store
的_changeController
獲取Stream
,並進行了一系列變換後,最終Stream
設定給了StreamBuilder
。 - 4、當我們呼叫
Stroe
的dispatch
方法時,我們會先進過NextDispatcher
陣列中的一系列middleware
攔截器,最終呼叫到隊末的_changeController
所在的NextDispatcher
。 - 5、最後一個
NextDispatcher
執行時會先執行reducer
方法獲取新的state
,然後通過_changeController.add
將狀態載入到Stream
流程中,觸發StoreConnector
的StreamBuilder
更新資料。
如果對於
Stream
流程不熟悉的還請看上篇。
現在再對照流程圖會不會清晰很多了?
在 flutter_redux
中,開發者的每個操作都只是一個 Action
,而這個行為所觸發的邏輯完全由 middleware
和 reducer
決定,這樣的設計在一定程度上將業務與UI隔離,同時也統一了狀態的管理。
比如你一個點選行為只是發出一個
RefrshAction
,但是通過middleware
攔截之後,在其中非同步處理完幾個資料介面,然後重新dispatch
出Action1
、Action2
、Action3
去更新其他頁面, 類似的redux_epics
庫就是這樣實現非同步的middleware
邏輯。
四、fish_redux
如果說 flutter_redux
屬於相對複雜的狀態管理設定的話,那麼閒魚開源的 fish_redux
可謂 “不走尋常路” 了,雖然是基於 redux
原有的設計理念,同時也有使用到 Stream
,但是相比較起來整個設計完全是 超脫三界,如果是前面的都是簡單的拼積木,那是 fish_redux
就是積木界的樂高。
因為篇幅原因,這裡也只展示部分程式碼,其中 reducer
還是我們熟悉的存在,而閒魚在這 redux
的基礎上提出了 Comoponent
的概念,這個概念下 fish_redux
是從 Context
、Widget
等地方就開始全面“入侵”你的程式碼,從而帶來“超級賽亞人”版的 redux
。
如下程式碼所示,預設情況我們需要:
- 繼承
Page
實現我們的頁面。 - 定義好我們的
State
狀態。 - 定義
effect
、middleware
、reducer
用於實現副作用、中介軟體、結果返回處理。 - 定義
view
用於繪製頁面。 - 定義
dependencies
使用者裝配控制元件,這裡最騷氣的莫過於過載了 + 操作符,然後利用Connector
從State
挑選出資料,然後通過Component
繪製。
現在看起來使用流程是不是變得複雜了?
但是這帶來的好處就是 複用的顆粒度更細了,裝配和功能更加的清晰。 那這個過程是如何實現的呢?後面我們將分析這個複雜的流程。
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 轉化處小 state
///然後將資料渲染到 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,
)
],
));
}
複製程式碼
如下大圖所示,整個聯動的流程比 flutter_redux
複雜了更多( 如果看不清可以點選大圖 ),而這個過程我們總結起來就是:
-
1、
Page
的構建需要State
、Effect
、Reducer
、view
、dependencies
、middleware
等引數。 -
2、
Page
的內部PageProvider
是一個InheritedWidget
使用者狀態共享。 -
3、
Page
內部會通過createMixedStore
建立Store
物件。 -
4、
Store
物件對外提供的subscribe
方法,在訂閱時會將訂閱的方法新增到內部List<_VoidCallback> _listeners
。 -
5、
Store
物件內部的StreamController.broadcast
建立出了_notifyController
物件用於廣播更新。 -
6、
Store
物件內部的subscribe
方法,會在ComponentState
中新增訂閱方法onNotify
,如果呼叫在onNotify
中最終會執行setState
更新UI。 -
7、
Store
物件對外提供的dispatch
方法,執行時內部會執行 4 中的List<_VoidCallback> _listeners
,觸發onNotify
。 -
8、
Page
內部會通過Logic
建立Dispatch
,執行時經歷Effect
->Middleware
->Stroe.dispatch
->Reducer
->State
->_notifyController
->_notifyController.add(state)
等流程。 -
9、以上流程最終就是
Dispatch
觸發Store
內部_notifyController
, 最終會觸發ComponentState
中的onNotify
中的setState
更新UI
是不是有很多物件很陌生?
確實 fish_redux
的整體流程更加複雜,內部的 ContxtSys
、Componet
、ViewSerivce
、 Logic
等等概念設計,這裡因為篇幅有限就不詳細拆分展示了,但從整個流程可以看出 fish_redux
從控制元件到頁面更新,全都進行了新的獨立設計,而這裡面最有意思的,莫不過 dependencies
。
如下圖所示,得益於fish_redux
內部 ConnOpMixin
中對操作符的過載,我們可以通過 DoubleCountConnector() + DoubleCountComponent()
來實現Dependent
的組裝。
Dependent
的組裝中 Connector
會從總 State 中讀取需要的小 State 用於 Component
的繪製,這樣很好的達到了 模組解耦與複用 的效果。
而使用中我們組裝的 dependencies
最後都會通過 ViewService
提供呼叫呼叫能力,比如呼叫 buildAdapter
用於列表能力,呼叫 buildComponent
提供獨立控制元件能力等。
可以看出 flutter_redux
的內部實現複雜度是比較高的,在提供組裝、複用、解耦的同時,也對專案進行了一定程度的入侵,這裡的篇幅可能不能很全面的分析 flutter_redux
中的整個流程,但是也能讓你理解整個流程的關鍵點,細細品味設計之美。
自此,第十二篇終於結束了!(///▽///)
資源推薦
- 本文Demo :github.com/CarGuo/stat…
- Github : github.com/CarGuo/
- 開源 Flutter 完整專案:github.com/CarGuo/GSYG…
- 開源 Flutter 多案例學習型專案: github.com/CarGuo/GSYF…
- 開源 Fluttre 實戰電子書專案:github.com/CarGuo/GSYF…