前言
在Flutter開發中,狀態管理是一個永恆的話題。
一般的原則是:如果狀態是組件私有的,則應該由組件自己管理;如果狀態要跨組件共享,則該狀態應該由各個組件共同的父元素來管理。
對於組件私有的狀態管理很好理解,但對於跨組件共享的狀態,管理的方式就比較多了,如使用全局事件總線EventBus,它是一個觀察者模式的實現,通過它就可以實現跨組件狀態同步:狀態持有方(發布者)負責更新、發布狀態,狀態使用方(觀察者)監聽狀態改變事件來執行一些操作。
但是觀察者模式來實現跨組件狀態共享有一些明顯的缺點:
- 必須顯示定義各種事件,不好管理;
- 訂閱者必須顯式注冊狀態改變回調,也必須在組件銷毀時手動去解綁回調以避免內存泄漏;
是否還有更好的跨組件狀態管理方式了?
我們知道,InheritedWidget能綁定與它依賴的子孫組件的依賴關系,並且當InheritedWidget數據發生變化時,可以自動更新依賴的子孫組件。基於此,可以將需要跨組件共享的狀態保存在InheritedWidget中,然后在子組件中引用InheritedWidget即可。Flutter社區的Provider包就是基於這個思想實現的。
Provider
基於上面的思想,實現一個最小功能的Provider。
定義一個保存共享數據的類
為了通用性,使用泛型。
class InheritedProvider<T> extends InheritedWidget{
InheritedProvider({@required this.data, Widget child}): super(child: child);
// 共享狀態使用泛型
final T data;
bool updateShouldNotify(InheritedProvider<T> old){
// 返回true,則每次更新都會調用依賴其的子孫節點的didChangeDependencies
return true;
}
}
數據發生變化時如何重構InheritedProvider
存在兩個問題:
- 數據發生變化怎么通知?
- 誰來重新構建InheritedProvider?
對於第一個問題,可以使用之前介紹的eventBus來進行事件通知,但是為了更貼近Flutter開發,使用Flutter中SDK中提供的ChangeNotifier類 ,它繼承自Listenable,也實現了一個Flutter風格的發布者-訂閱者模式,可以通過調用addListener()和removeListener()來添加、移除監聽器(訂閱者);通過調用notifyListeners() 可以觸發所有監聽器回調。
對於第二個問題,將要共享的狀態放到一個Model類中,然后讓它繼承自ChangeNotifier,這樣當共享的狀態改變時,我們只需要調用notifyListeners() 來通知訂閱者,然后由訂閱者來重新構建InheritedProvider。
// 該方法用於Dart獲取模板類型
Type _typeOf<T>() => T;
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget{
ChangeNotifierProvider({
Key key,
this.data,
this.child,
});
final Widget child;
final T data;
// 定義一個便捷方法,方便子樹中的widget獲取共享數據
// static T of<T>(BuildContext context){
// final type = _typeOf<InheritedProvider<T>>();
// final provider = context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>;
// return provider.data;
// }
// 替換上面的便捷方法,按需求是否注冊依賴關系
static T of<T>(BuildContext context, {bool listen = true}){
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget as InheritedProvider<T>;
return provider.data;
}
_ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}
// _ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。
// 注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,
// 所以執行build時,InheritedProvider的child引用的始終是同一個子widget,
// 所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
void update(){
// 如果數據發生變化(model類調用了notifyListeners),重新構建InheritedProvider
setState(() => {});
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
// 當Provider更新時,如果新舊數據不相等,則解綁舊數據監聽,同時添加新數據監聽
if(widget.data != oldWidget.data){
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState(){
// 給model添加監聽器
widget.data.addListener(update);
super.initState();
}
@override
void dispose(){
// 移除model的監聽器
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context){
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
}
}
可以看到_ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,所以執行build時,InheritedProvider的child引用的始終是同一個子widget,所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。
代碼示例
// 跨組件狀態共享(Provider)
// 一個通用的InheritedWidget,保存任需要跨組件共享的狀態
import 'dart:collection';
import 'package:flutter/material.dart';
class InheritedProvider<T> extends InheritedWidget{
InheritedProvider({@required this.data, Widget child}): super(child: child);
// 共享狀態使用泛型
final T data;
bool updateShouldNotify(InheritedProvider<T> old){
// 返回true,則每次更新都會調用依賴其的子孫節點的didChangeDependencies
return true;
}
}
// 該方法用於Dart獲取模板類型
Type _typeOf<T>() => T;
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget{
ChangeNotifierProvider({
Key key,
this.data,
this.child,
});
final Widget child;
final T data;
// 定義一個便捷方法,方便子樹中的widget獲取共享數據
// static T of<T>(BuildContext context){
// final type = _typeOf<InheritedProvider<T>>();
// final provider = context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>;
// return provider.data;
// }
// 替換上面的便捷方法,按需求是否注冊依賴關系
static T of<T>(BuildContext context, {bool listen = true}){
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget as InheritedProvider<T>;
return provider.data;
}
_ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}
// _ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。
// 注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,
// 所以執行build時,InheritedProvider的child引用的始終是同一個子widget,
// 所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
void update(){
// 如果數據發生變化(model類調用了notifyListeners),重新構建InheritedProvider
setState(() => {});
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
// 當Provider更新時,如果新舊數據不相等,則解綁舊數據監聽,同時添加新數據監聽
if(widget.data != oldWidget.data){
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState(){
// 給model添加監聽器
widget.data.addListener(update);
super.initState();
}
@override
void dispose(){
// 移除model的監聽器
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context){
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
}
}
// 購物車示例:實現一個顯示購物車中所有商品總價的功能
// 用於表示商品信息
class Item{
// 商品單價
double price;
// 商品份數
int count;
Item(this.price, this.count);
}
// 保存購物車內商品數據,跨組件共享
class CartModel extends ChangeNotifier{
// 用於保存購物車中商品列表
final List<Item> _items = [];
// 禁止改變購物車里的商品信息
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
// 購物車中商品的總價
double get totalPrice => _items.fold(0, (value, item) => value + item.count * item.price);
// 將[item]添加到購物車,這是唯一一種能從外部改變購物車的方法
void add(Item item) {
_items.add(item);
// 通知監聽者(訂閱者),重新構建InheritedProvider,更新狀態
notifyListeners();
}
}
// 優化
// 一個便捷類,封裝一個Consumer的Widget
class Consumer<T> extends StatelessWidget{
final Widget child;
final Widget Function(BuildContext context, T value) builder;
Consumer({
Key key,
@required this.builder,
this.child,
}): assert(builder != null),
super(key: key);
Widget build(BuildContext context){
return builder(
context,
// 自動獲取Model
ChangeNotifierProvider.of<T>(context),
);
}
}
// 頁面
class ProviderRoute extends StatefulWidget{
_ProviderRouteState createState() => _ProviderRouteState();
}
class _ProviderRouteState extends State<ProviderRoute>{
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('跨組件狀態共享(Provider)'),
),
body: 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("總價:${cart.totalPrice}");
// },),
// 進行優化,替換上面Builder
Consumer<CartModel>(
builder: (context, cart) => Text("總價:${cart.totalPrice}"),
),
Builder(builder: (context){
// 控制台打印出這句,說明按鈕在每次點擊時其自身都會重新build!
print("RaisedButton build");
return RaisedButton(
child: Text("添加商品"),
onPressed: (){
// 給購物車中添加商品,添加后總價會更新
// ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
// listen設為false,不建立依賴關系,因為按鈕不需要每次重新build
ChangeNotifierProvider.of<CartModel>(context, listen: false).add(Item(20.0, 1));
},
);
},)
],
);
},),
),
),
);
}
}
代碼優化
兩個地方可以進行代碼優化,詳細看代碼示例。
總結
Provider原理圖
Model變化后會自動通知ChangeNotifierProvider(訂閱者),ChangeNotifierProvider內部會重新構建InheritedWidget,而依賴該InheritedWidget的子孫Widget就會更新。
可以發現使用Provider,將會帶來如下收益:
- 業務代碼更關注數據了,只要更新Model,則UI會自動更新,而不用在狀態改變后再去手動調用setState()來顯式更新頁面。
- 數據改變的消息傳遞被屏蔽了,我們無需手動去處理狀態改變事件的發布和訂閱了,這一切都被封裝在Provider中了。這真的很棒,幫我們省掉了大量的工作!
- 在大型復雜應用中,尤其是需要全局共享的狀態非常多時,使用Provider將會大大簡化我們的代碼邏輯,降低出錯的概率,提高開發效率。
其他狀態管理包
Provider & Scoped Model、Redux、MobX、BLoC,這里特別推薦阿里咸魚團隊推出的開源項目:fish redux。