Flutter 狀態管理之Provider
在flutter
中狀態管理是重中之重,每當談這個話題,總有說不完的話。
在正式介紹 Provider
為什麼我們需要狀態管理。如果你已經對此十分清楚,那麼建議直接跳過這一節。
如果我們的應用足夠簡單,Flutter
作為一個宣告式框架,你或許只需要將 資料 對映成 檢視 就可以了。你可能並不需要狀態管理,就像下面這樣。
但是隨著功能的增加,你的應用程式將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。
這又是什麼鬼。我們很難再清楚的測試維護我們的狀態,因為它看上去實在是太複雜了!而且還會有多個頁面共享同一個狀態,例如當你進入一個文章點贊,退出到外部縮略展示的時候,外部也需要顯示點贊數,這時候就需要同步這兩個狀態。
Flutter
實際上在一開始就為我們提供了一種狀態管理方式,那就是 StatefulWidget
。但是我們很快發現,它正是造成上述原因的罪魁禍首。在
State
屬於某一個特定的 Widget
,在多個 Widget
之間進行交流的時候,雖然你可以使用 callback
解決,但是當巢狀足夠深的話,我們增加非常多可怕的垃圾程式碼。這時候,我們便迫切的需要一個架構來幫助我們理清這些關係,狀態管理框架應運而生。
Provider 是什麼
通過使用
Provider
而不用手動編寫InhertedWidget,您將獲取諮詢分配、延遲載入、打打減少每次建立新類的程式碼。
首先在yaml
中新增,具體版本號參考:4.1.3
.
Provider: ^4.1.3
然後執行
flutter pub get
獲取到最新的包到本地,在需要的資料夾內匯入
import 'package:provider/provider.dart';
簡單例子
我們還用點選按鈕新增數字的例子
首先建立儲存資料的Model
class ProviderModel extends ChangeNotifier { int _count=0; ProviderModel(); void plus() { /// 在資料變動的時候通知監聽者重新整理UI_count = _count + 1; notifyListeners(); } }
構造view
/// 使用Consumer來監聽全域性重新整理UI Consumer<ProviderModel>( builder: (BuildContext context, ProviderModel value, Widget child) { print('Consumer 0 重新整理'); _string += 'c0 '; return _Row( value: value._count.toString(), callback: () { context.read<ProviderModel>().plus(); }, ); }, child: _Row( value: '0', callback: () { context.read<ProviderModel>().plus(); }, ), )
測試下看下效果:
單個Model多個小部件分別重新整理(區域性重新整理)
單個model
實現單個頁面多個小部件分別重新整理,是使用Selector<Model,int>
來實現,首先看下建構函式:
class Selector<A, S> extends Selector0<S> { /// {@macro provider.selector} Selector({ Key key, @required ValueWidgetBuilder<S> builder, @required S Function(BuildContext, A) selector, ShouldRebuild<S> shouldRebuild, Widget child, }) : assert(selector != null), super( key: key, shouldRebuild: shouldRebuild, builder: builder, selector: (context) => selector(context, Provider.of(context)), child: child, ); }
可以看到Selector
繼承了Selector0
,再看Selector
關鍵build
程式碼:
class _Selector0State<T> extends SingleChildState<Selector0<T>> { T value; Widget cache; Widget oldWidget; @override Widget buildWithChild(BuildContext context, Widget child) { final selected = widget.selector(context); var shouldInvalidateCache = oldWidget != widget || (widget._shouldRebuild != null && widget._shouldRebuild.call(value, selected)) || (widget._shouldRebuild == null && !const DeepCollectionEquality().equals(value, selected)); if (shouldInvalidateCache) { value = selected; oldWidget = widget; cache = widget.builder( context, selected, child, ); } return cache; } }
根據我們傳入的_shouldRebuild
來判斷是否需要更新,如果需要更新則執行widget.build(context,selected,child)
,否則返回已經快取的cache
.當沒有_shouldRebuild
引數時則根據widget.selector(ctx)
的返回值判斷是否和舊值相等,不等則更新UI
。
所以我們不寫shouldRebuild
也是可以的。
區域性重新整理用法
Widget build(BuildContext context) { print('page 1'); _string += 'page '; return Scaffold( appBar: AppBar( title: Text('Provider 全域性與區域性重新整理'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text('全域性重新整理<Consumer>'), Consumer<ProviderModel>( builder: (BuildContext context, ProviderModel value, Widget child) { print('Consumer 0 重新整理'); _string += 'c0 '; return _Row( value: value._count.toString(), callback: () { context.read<ProviderModel>().plus(); }, ); }, child: _Row( value: '0', callback: () { context.read<ProviderModel>().plus(); }, ), ), SizedBox( height: 40, ), Text('區域性重新整理<Selector>'), Selector<ProviderModel, int>( builder: (ctx, value, child) { print('Selector 1 重新整理'); _string += 's1 '; return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Selector<Model,int>次數:' + value.toString()), OutlineButton( onPressed: () { context.read<ProviderModel>().plus2(); }, child: Icon(Icons.add), ) ], ); }, selector: (ctx, model) => model._count2, shouldRebuild: (m1, m2) { print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次重新整理' : '資料相等,本次不重新整理'}'); return m1 != m2; }, ), SizedBox( height: 40, ), Text('區域性重新整理<Selector>'), Selector<ProviderModel, int>( selector: (context, model) => model._count3, shouldRebuild: (m1, m2) { print('s2:$m1 $m2 ${m1 != m2 ? '不相等,本次重新整理' : '資料相等,本次不重新整理'}'); return m1 != m2; }, builder: (ctx, value, child) { print('selector 2 重新整理'); _string += 's2 '; return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Selector<Model,int>次數:' + value.toString()), OutlineButton( onPressed: () { ctx.read<ProviderModel>().plus3(); }, child: Icon(Icons.add), ) ], ); }, ), SizedBox( height: 40, ), Text('重新整理次數和順序:↓'), Text(_string), Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ OutlineButton( child: Icon(Icons.refresh), onPressed: () { setState(() { _string += '\n'; }); }, ), OutlineButton( child: Icon(Icons.close), onPressed: () { setState(() { _string = ''; }); }, ) ], ) ], ), ), ); }
效果:
當我們點選區域性重新整理s1
,執行s1
的build
,s1
不相等,s2
相等不重新整理。輸出:
flutter: s2:5 5 資料相等,本次不重新整理 flutter: s1:6 7 不相等,本次重新整理 flutter: Selector 1 重新整理 flutter: Consumer 0 重新整理
當點選s2
,s2
的值不相等重新整理UI
,s1
資料相等,不重新整理UI
.
flutter: s2:2 3 不相等,本次重新整理 flutter: selector 2 重新整理 flutter: s1:0 0 資料相等,本次不重新整理 flutter: Consumer 0 重新整理
可以看到上邊2次Consumer
每次都重新整理了,我們探究下原因。
Consumer 全域性重新整理
Consumer
繼承了SingleCHildStatelessWidget
,當我們在ViewModel
中呼叫notification
則當前widget
被標記為dirty
,然後在build
中執行傳入的builder
函式,在下幀則會重新整理UI
。
而Selector<T,S>
則被標記dirty
時執行_Selector0State
中的buildWithChild(ctx,child)
函式時,根據selected
和_shouldRebuild
來判斷是否需要執行widget.builder(ctx,selected,child)
(重新整理UI
).
其他用法
多model寫法
只需要在所有需要model
的上級包裹即可,當我們一個page
需要2
個model
的時候,我麼通常這樣子寫:
class BaseProviderRoute extends StatelessWidget { BaseProviderRoute({Key key}) : super(key: key); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<ProviderModel>( create: (_) => ProviderModel(), ), ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()), ], child: BaseProvider(), ); } }
當然是用的時候和單一model
一致的。
Selector<ProviderModel2, int>( selector: (context, model) => model.value, builder: (ctx, value, child) { print('model2 s1 重新整理'); _string += 'm2s1 '; return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Selector<Model2,int>次數:' + value.toString()), OutlineButton( onPressed: () { ctx.read<ProviderModel2>().add(2); }, child: Icon(Icons.add), ) ], ); }, ),
watch && read
watch
原始碼是Provider.of<T>(this)
,預設Provider.of<T>(this)
的listen=true
.
static T of<T>(BuildContext context, {bool listen = true}){ final inheritedElement = _inheritedElementOf<T>(context); if (listen) { context.dependOnInheritedElement(inheritedElement); } return inheritedElement.value; }
而read
原始碼是Provider.of<T>(this, listen: false)
,watch
/read
只是寫法簡單一點,並無高深結構。
當我們想要監聽值的變化則是用
watch
,當想呼叫model
的函式時則使用read
參考
- 程式碼倉庫: https://github.com/ifgyong/flutter-example
- 官方Provider: https://github.com/rrousselGit/provider