在 Flutter 開發(fā)中,狀態(tài)管理是一個(gè)永恒的話題。一般的原則是:如果狀態(tài)是組件私有的,則應(yīng)該由組件自己管理;如果狀態(tài)要跨組件共享,則該狀態(tài)應(yīng)該由各個(gè)組件共同的父元素來管理。對(duì)于組件私有的狀態(tài)管理很好理解,但對(duì)于跨組件共享的狀態(tài),管理的方式就比較多了,如使用全局事件總線 EventBus(將在下一章中介紹),它是一個(gè)觀察者模式的實(shí)現(xiàn),通過它就可以實(shí)現(xiàn)跨組件狀態(tài)同步:狀態(tài)持有方(發(fā)布者)負(fù)責(zé)更新、發(fā)布狀態(tài),狀態(tài)使用方(觀察者)監(jiān)聽狀態(tài)改變事件來執(zhí)行一些操作。下面我們看一個(gè)登陸狀態(tài)同步的簡(jiǎn)單示例:
定義事件:
enum Event{
login,
... //省略其它事件
}
登錄頁代碼大致如下:
// 登錄狀態(tài)改變后發(fā)布狀態(tài)改變事件
bus.emit(Event.login);
依賴登錄狀態(tài)的頁面:
void onLoginChanged(e){
//登錄狀態(tài)變化處理邏輯
}
@override
void initState() {
//訂閱登錄狀態(tài)改變事件
bus.on(Event.login,onLogin);
super.initState();
}
@override
void dispose() {
//取消訂閱
bus.off(Event.login,onLogin);
super.dispose();
}
我們可以發(fā)現(xiàn),通過觀察者模式來實(shí)現(xiàn)跨組件狀態(tài)共享有一些明顯的缺點(diǎn):
在 Flutter 當(dāng)中有沒有更好的跨組件狀態(tài)管理方式了呢?答案是肯定的,那怎么做的?我們想想前面介紹的InheritedWidget
,它的天生特性就是能綁定InheritedWidget
與依賴它的子孫組件的依賴關(guān)系,并且當(dāng)InheritedWidget
數(shù)據(jù)發(fā)生變化時(shí),可以自動(dòng)更新依賴的子孫組件!利用這個(gè)特性,我們可以將需要跨組件共享的狀態(tài)保存在InheritedWidget
中,然后在子組件中引用InheritedWidget
即可,F(xiàn)lutter 社區(qū)著名的 Provider 包正是基于這個(gè)思想實(shí)現(xiàn)的一套跨組件狀態(tài)共享解決方案,接下來我們便詳細(xì)介紹一下 Provider 的用法及原理。
為了加強(qiáng)讀者的理解,我們不直接去看 Provider 包的源代碼,相反,我會(huì)帶著你根據(jù)上面描述的通過InheritedWidget
實(shí)現(xiàn)的思路來一步一步地實(shí)現(xiàn)一個(gè)最小功能的 Provider。
首先,我們需要一個(gè)保存需要共享的數(shù)據(jù)InheritedWidget
,由于具體業(yè)務(wù)數(shù)據(jù)類型不可預(yù)期,為了通用性,我們使用泛型,定義一個(gè)通用的InheritedProvider
類,它繼承自InheritedWidget
:
// 一個(gè)通用的InheritedWidget,保存任需要跨組件共享的狀態(tài)
class InheritedProvider<T> extends InheritedWidget {
InheritedProvider({@required this.data, Widget child}) : super(child: child);
//共享狀態(tài)使用泛型
final T data;
@override
bool updateShouldNotify(InheritedProvider<T> old) {
//在此簡(jiǎn)單返回true,則每次更新都會(huì)調(diào)用依賴其的子孫節(jié)點(diǎn)的`didChangeDependencies`。
return true;
}
}
數(shù)據(jù)保存的地方有了,那么接下來我們需要做的就是在數(shù)據(jù)發(fā)生變化的時(shí)候來重新構(gòu)建InheritedProvider
,那么現(xiàn)在就面臨兩個(gè)問題:
InheritedProvider
?
第一個(gè)問題其實(shí)很好解決,我們當(dāng)然可以使用之前介紹的 eventBus 來進(jìn)行事件通知,但是為了更貼近 Flutter 開發(fā),我們使用 Flutter SDK 中提供的ChangeNotifier
類 ,它繼承自Listenable
,也實(shí)現(xiàn)了一個(gè) Flutter 風(fēng)格的發(fā)布者-訂閱者模式,ChangeNotifier
定義大致如下:
class ChangeNotifier implements Listenable {
List listeners=[];
@override
void addListener(VoidCallback listener) {
//添加監(jiān)聽器
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
//移除監(jiān)聽器
listeners.remove(listener);
}
void notifyListeners() {
//通知所有監(jiān)聽器,觸發(fā)監(jiān)聽器回調(diào)
listeners.forEach((item)=>item());
}
... //省略無關(guān)代碼
}
我們可以通過調(diào)用addListener()
和removeListener()
來添加、移除監(jiān)聽器(訂閱者);通過調(diào)用notifyListeners()
可以觸發(fā)所有監(jiān)聽器回調(diào)。
現(xiàn)在,我們將要共享的狀態(tài)放到一個(gè) Model 類中,然后讓它繼承自ChangeNotifier
,這樣當(dāng)共享的狀態(tài)改變時(shí),我們只需要調(diào)用notifyListeners()
來通知訂閱者,然后由訂閱者來重新構(gòu)建InheritedProvider
,這也是第二個(gè)問題的答案!接下來我們便實(shí)現(xiàn)這個(gè)訂閱者類:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
ChangeNotifierProvider({
Key key,
this.data,
this.child,
});
final Widget child;
final T data;
//定義一個(gè)便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
static T of<T>(BuildContext context) {
final type = _typeOf<InheritedProvider<T>>();
final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
return provider.data;
}
@override
_ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}
該類繼承StatefulWidget
,然后定義了一個(gè)of()
靜態(tài)方法供子類方便獲取 Widget 樹中的InheritedProvider
中保存的共享狀態(tài)(model),下面我們實(shí)現(xiàn)該類對(duì)應(yīng)的_ChangeNotifierProviderState
類:
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
void update() {
//如果數(shù)據(jù)發(fā)生變化(model類調(diào)用了notifyListeners),重新構(gòu)建InheritedProvider
setState(() => {});
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
//當(dāng)Provider更新時(shí),如果新舊數(shù)據(jù)不"==",則解綁舊數(shù)據(jù)監(jiān)聽,同時(shí)添加新數(shù)據(jù)監(jiān)聽
if (widget.data != oldWidget.data) {
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
// 給model添加監(jiān)聽器
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
// 移除model的監(jiān)聽器
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context) {
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
}
}
可以看到_ChangeNotifierProviderState
類的主要作用就是監(jiān)聽到共享狀態(tài)(model)改變時(shí)重新構(gòu)建 Widget 樹。注意,在_ChangeNotifierProviderState
類中調(diào)用setState()
方法,widget.child
始終是同一個(gè),所以執(zhí)行build時(shí),InheritedProvider
的 child 引用的始終是同一個(gè)子widget,所以widget.child
并不會(huì)重新build
,這也就相當(dāng)于對(duì)child
進(jìn)行了緩存!當(dāng)然如果ChangeNotifierProvider
父級(jí)Widget重新build時(shí),則其傳入的child
便有可能會(huì)發(fā)生變化。
現(xiàn)在我們所需要的各個(gè)工具類都已完成,下面我們通過一個(gè)購物車的例子來看看怎么使用上面的這些類。
我們需要實(shí)現(xiàn)一個(gè)顯示購物車中所有商品總價(jià)的功能:
定義一個(gè)Item
類,用于表示商品信息:
class Item {
Item(this.price, this.count);
double price; //商品單價(jià)
int count; // 商品份數(shù)
//... 省略其它屬性
}
定義一個(gè)保存購物車內(nèi)商品數(shù)據(jù)的CartModel
類:
class CartModel extends ChangeNotifier {
// 用于保存購物車中商品列表
final List<Item> _items = [];
// 禁止改變購物車?yán)锏纳唐沸畔? UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
// 購物車中商品的總價(jià)
double get totalPrice =>
_items.fold(0, (value, item) => value + item.count * item.price);
// 將 [item] 添加到購物車。這是唯一一種能從外部改變購物車的方法。
void add(Item item) {
_items.add(item);
// 通知監(jiān)聽器(訂閱者),重新構(gòu)建InheritedProvider, 更新狀態(tài)。
notifyListeners();
}
}
CartModel
即要跨組件共享的 model 類。最后我們構(gòu)建示例頁面:
class ProviderRoute extends StatefulWidget {
@override
_ProviderRouteState createState() => _ProviderRouteState();
}
class _ProviderRouteState extends State<ProviderRoute> {
@override
Widget build(BuildContext context) {
return Center(
child: ChangeNotifierProvider<CartModel>(
data: CartModel(),
child: Builder(builder: (context) {
return Column(
children: <Widget>[
Builder(builder: (context){
var cart=ChangeNotifierProvider.of<CartModel>(context);
return Text("總價(jià): ${cart.totalPrice}");
}),
Builder(builder: (context){
print("RaisedButton build"); //在后面優(yōu)化部分會(huì)用到
return RaisedButton(
child: Text("添加商品"),
onPressed: () {
//給購物車中添加商品,添加后總價(jià)會(huì)更新
ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
},
);
}),
],
);
}),
),
);
}
}
運(yùn)行示例后效果如圖7-2所示:
每次點(diǎn)擊”添加商品“按鈕,總價(jià)就會(huì)增加20,我們期望的功能實(shí)現(xiàn)了!可能有些讀者會(huì)疑惑,我們饒了一大圈實(shí)現(xiàn)這么簡(jiǎn)單的功能有意義么?其實(shí),就這個(gè)例子來看,只是更新同一個(gè)路由頁中的一個(gè)狀態(tài),我們使用ChangeNotifierProvider
的優(yōu)勢(shì)并不明顯,但是如果我們是做一個(gè)購物 APP 呢?由于購物車數(shù)據(jù)是通常是會(huì)在整個(gè) APP 中共享的,比如會(huì)跨路由共享。如果我們將ChangeNotifierProvider
放在整個(gè)應(yīng)用的 Widget 樹的根上,那么整個(gè) APP 就可以共享購物車的數(shù)據(jù)了,這時(shí)ChangeNotifierProvider
的優(yōu)勢(shì)將會(huì)非常明顯。
雖然上面的例子比較簡(jiǎn)單,但它卻將 Provider 的原理和流程體現(xiàn)的很清楚,圖7-3是 Provider 的原理圖:
Model 變化后會(huì)自動(dòng)通知ChangeNotifierProvider
(訂閱者),ChangeNotifierProvider
內(nèi)部會(huì)重新構(gòu)建InheritedWidget
,而依賴該InheritedWidget
的子孫 Widget 就會(huì)更新。
我們可以發(fā)現(xiàn)使用Provider,將會(huì)帶來如下收益:
setState()
來顯式更新頁面。
我們上面實(shí)現(xiàn)的ChangeNotifierProvider
是有兩個(gè)明顯缺點(diǎn):代碼組織問題和性能問題,下面我們一一討論。
我們先看一下構(gòu)建顯示總價(jià) Text 的代碼:
Builder(builder: (context){
var cart=ChangeNotifierProvider.of<CartModel>(context);
return Text("總價(jià): ${cart.totalPrice}");
})
這段代碼有兩點(diǎn)可以優(yōu)化:
ChangeNotifierProvider.of
,當(dāng) APP 內(nèi)部依賴CartModel
很多時(shí),這樣的代碼將很冗余。ChangeNotifierProvider
是訂閱者,那么依賴CartModel
的 Widget 自然就是訂閱者,其實(shí)也就是狀態(tài)的消費(fèi)者,如果我們用Builder
來構(gòu)建,語義就不是很明確;如果我們能使用一個(gè)具有明確語義的 Widget,比如就叫Consumer
,這樣最終的代碼語義將會(huì)很明確,只要看到Consumer
,我們就知道它是依賴某個(gè)跨組件或全局的狀態(tài)。
為了優(yōu)化這兩個(gè)問題,我們可以封裝一個(gè)Consumer
Widget,實(shí)現(xiàn)如下:
// 這是一個(gè)便捷類,會(huì)獲得當(dāng)前context和指定數(shù)據(jù)類型的Provider
class Consumer<T> extends StatelessWidget {
Consumer({
Key key,
@required this.builder,
this.child,
}) : assert(builder != null),
super(key: key);
final Widget child;
final Widget Function(BuildContext context, T value) builder;
@override
Widget build(BuildContext context) {
return builder(
context,
ChangeNotifierProvider.of<T>(context), //自動(dòng)獲取Model
);
}
}
Consumer
實(shí)現(xiàn)非常簡(jiǎn)單,它通過指定模板參數(shù),然后再內(nèi)部自動(dòng)調(diào)用ChangeNotifierProvider.of
獲取相應(yīng)的 Model,并且Consumer
這個(gè)名字本身也是具有確切語義(消費(fèi)者)?,F(xiàn)在上面的代碼塊可以優(yōu)化為如下這樣:
Consumer<CartModel>(
builder: (context, cart)=> Text("總價(jià): ${cart.totalPrice}");
)
是不是很優(yōu)雅!
上面的代碼還有一個(gè)性能問題,就在構(gòu)建”添加按鈕“的代碼處:
Builder(builder: (context) {
print("RaisedButton build"); // 構(gòu)建時(shí)輸出日志
return RaisedButton(
child: Text("添加商品"),
onPressed: () {
ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
},
);
}
我們點(diǎn)擊”添加商品“按鈕后,由于購物車商品總價(jià)會(huì)變化,所以顯示總價(jià)的 Text 更新是符合預(yù)期的,但是”添加商品“按鈕本身沒有變化,是不應(yīng)該被重新 build 的。但是我們運(yùn)行示例,每次點(diǎn)擊”添加商品“按鈕,控制臺(tái)都會(huì)輸出"RaisedButton build"日志,也就是說”添加商品“按鈕在每次點(diǎn)擊時(shí)其自身都會(huì)重新 build!這是為什么呢?如果你已經(jīng)理解了InheritedWidget
的更新機(jī)制,那么答案一眼就能看出:這是因?yàn)闃?gòu)建RaisedButton
的Builder
中調(diào)用了ChangeNotifierProvider.of
,也就是說依賴了 Widget 樹上面的InheritedWidget
(即InheritedProvider
)Widget,所以當(dāng)添加完商品后,CartModel
發(fā)生變化,會(huì)通知ChangeNotifierProvider
, 而ChangeNotifierProvider
則會(huì)重新構(gòu)建子樹,所以InheritedProvider
將會(huì)更新,此時(shí)依賴它的子孫 Widget 就會(huì)被重新構(gòu)建。
問題的原因搞清楚了,那么我們?nèi)绾伪苊膺@不必要重構(gòu)呢?既然按鈕重新被 build 是因?yàn)榘粹o和InheritedWidget
建立了依賴關(guān)系,那么我們只要打破或解除這種依賴關(guān)系就可以了。那么如何解除按鈕和InheritedWidget
的依賴關(guān)系呢?我們上一節(jié)介紹InheritedWidget
時(shí)已經(jīng)講過了:調(diào)用dependOnInheritedWidgetOfExactType()
和 getElementForInheritedWidgetOfExactType()
的區(qū)別就是前者會(huì)注冊(cè)依賴關(guān)系,而后者不會(huì)。所以我們只需要將ChangeNotifierProvider.of
的實(shí)現(xiàn)改為下面這樣即可:
//添加一個(gè)listen參數(shù),表示是否建立依賴關(guān)系
static T of<T>(BuildContext context, {bool listen = true}) {
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
: context.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()?.widget
as InheritedProvider<T>;
return provider.data;
}
然后我們將調(diào)用部分代碼改為:
Column(
children: <Widget>[
Consumer<CartModel>(
builder: (BuildContext context, cart) =>Text("總價(jià): ${cart.totalPrice}"),
),
Builder(builder: (context) {
print("RaisedButton build");
return RaisedButton(
child: Text("添加商品"),
onPressed: () {
// listen 設(shè)為false,不建立依賴關(guān)系
ChangeNotifierProvider.of<CartModel>(context, listen: false)
.add(Item(20.0, 1));
},
);
})
],
)
修改后再次運(yùn)行上面的示例,我們會(huì)發(fā)現(xiàn)點(diǎn)擊”添加商品“按鈕后,控制臺(tái)不會(huì)再輸出"RaisedButton build"了,即按鈕不會(huì)被重新構(gòu)建了。而總價(jià)仍然會(huì)更新,這是因?yàn)?code>Consumer中調(diào)用ChangeNotifierProvider.of
時(shí)listen
值為默認(rèn)值 true,所以還是會(huì)建立依賴關(guān)系。
至此我們便實(shí)現(xiàn)了一個(gè)迷你的 Provider,它具備 Pub上Provider Package 中的核心功能;但是我們的迷你版功能并不全面,如只實(shí)現(xiàn)了一個(gè)可監(jiān)聽的 ChangeNotifierProvider,并沒有實(shí)現(xiàn)只用于數(shù)據(jù)共享的 Provider;另外,我們的實(shí)現(xiàn)有些邊界也沒有考慮的到,比如如何保證在 Widget 樹重新 build 時(shí) Model 始終是單例等。所以建議讀者在實(shí)戰(zhàn)中還是使用 Provider Package,而本節(jié)實(shí)現(xiàn)這個(gè)迷你 Provider 的主要目的主要是為了幫助讀者了解 Provider Package 底層的原理。
現(xiàn)在 Flutter 社區(qū)已經(jīng)有很多專門用于狀態(tài)管理的包了,在此我們列出幾個(gè)相對(duì)評(píng)分比較高的:
包名 | 介紹 |
---|---|
Provider (opens new window)& Scoped Model(opens new window) | 這兩個(gè)包都是基于InheritedWidget 的,原理相似 |
Redux(opens new window) | 是 Web 開發(fā)中 React 生態(tài)鏈中 Redux 包的 Flutter 實(shí)現(xiàn) |
MobX(opens new window) | 是 Web 開發(fā)中 React 生態(tài)鏈中 MobX 包的 Flutter 實(shí)現(xiàn) |
BLoC(opens new window) | 是 BLoC 模式的 Flutter 實(shí)現(xiàn) |
在此筆者不對(duì)這些包做推薦,讀者有興趣都可以研究一下,了解它們各自的思想。
本節(jié)通過介紹事件總線在跨組件共享中的一些缺點(diǎn)引出了通過InheritedWidget
來實(shí)現(xiàn)狀態(tài)的共享的思想,然后基于該思想實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 Provider,在實(shí)現(xiàn)的過程中也更深入的探索了InheritedWidget
與其依賴項(xiàng)的注冊(cè)機(jī)制和更新機(jī)制。通過本節(jié)的學(xué)習(xí),讀者應(yīng)該達(dá)到兩個(gè)目標(biāo),首先是對(duì)InheritedWidget
徹底吃透,其次是 Provider 的設(shè)計(jì)思想。
InheritedWidget
是 Flutter 中非常重要的一個(gè) Widget,像國際化、主題等都是通過它來實(shí)現(xiàn),所以我們也不惜篇幅,通過好幾節(jié)來介紹它的,在下一節(jié)中,我們將介紹另一個(gè)基于InheritedWidget
的組件 Theme(主題)。
更多建議: