Flutter 状态管理指南之 Provider
2019 Google I/O 大會,Flutter 團隊在“Pragmatic State Management in Flutter?”演講上正式介紹了 Provider。自此,Provider?代替 Provide 成為官方推薦的狀態(tài)管理方式之一。
本文將基于最新 Provider v-3.0 進行介紹,除了講解其使用方式之外,重點介紹了 Provider 不同“提供”方式的適用場景,以及在使用狀態(tài)管理時候需要遵守的原則。這將會讓你在編寫 Flutter App 的過程中很大程度上減輕思考負擔。
推薦閱讀時間:1小時
本文結(jié)構(gòu):
1.為什么需要狀態(tài)管理
2.什么是 Provider
3.創(chuàng)建一個簡單計數(shù)器 app
4.你還需要知道的
5.Tips
6.Q&A
7.源碼淺析
一、為什么需要狀態(tài)管理
在弄清楚如何使用 Provider 之前,我們首先要了解為什么我的應(yīng)用需要狀態(tài)管理。如果你已經(jīng)對此十分清楚,那么建議直接跳過這一節(jié)。
如果我們的應(yīng)用足夠簡單,Flutter 作為一個聲明式框架,你或許只需要將數(shù)據(jù)映射成視圖就可以了。你可能并不需要狀態(tài)管理,就像下面這樣。
但是隨著功能的增加,你的應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)。這個時候你的應(yīng)用應(yīng)該會是這樣。
隨著你的應(yīng)用需要共享多處統(tǒng)一狀態(tài)時,我們很難再清楚的測試維護我們的狀態(tài),因為它看上去實在是太復雜了!而且還會有多個頁面共享同一個狀態(tài),例如當你進入一個文章點贊,退出到外部縮略展示的時候,外部也需要顯示點贊數(shù),這時候就需要同步這兩個狀態(tài)。
Flutter 實際上在一開始就為我們提供了一種狀態(tài)管理方式 — StatefulWidget。然而我們發(fā)現(xiàn)它僅適合用于在單個 Widget 內(nèi)部維護其狀態(tài)。當我們需要使用跨組件的狀態(tài)時,StatefulWidget 將不再是一個好的選擇。
State 屬于某一個特定的 StatefulWidget,在多個 Widget 之間進行交流的時候,雖然你可以使用 callback 解決,但是當嵌套足夠深的話,很容易就增大代碼耦合度。
這時候,我們便迫切的需要一個架構(gòu)來幫助我們理清這些關(guān)系,狀態(tài)管理框架應(yīng)運而生。
二、什么是 Provider
那么我們該如何解決上面這種糟糕的情況呢。在上手了?Provider?之后,我可以說這個庫是一個相當不錯的解決方案。我們先來簡單說一下 Provider 的基本作用。
Provider 從名字上就很容易理解,它用來提供數(shù)據(jù),而它的優(yōu)秀之處在于無論是在單個頁面還是在整個?app 都有相應(yīng)的解決方案,我們可以很方便的管理狀態(tài),并在合適的時機釋放資源。可以說,Provider 的目標就是完全替代?StatefulWidget。
說了很多還是很抽象,我們先一起做一個最簡單的例子。
三、創(chuàng)建一個簡單計數(shù)器 app
這里我們用這個 Counter App 為例,給大家介紹如何在兩個獨立的頁面中共享計數(shù)器(counter)的狀態(tài)應(yīng)該怎么做,具體會像下面這樣。
兩個頁面中心字體共用了同一個字體大小。
第二個頁面的按鈕將會讓數(shù)字增加(第一個頁面的數(shù)字將會同步增加。)
3.1 第一步:添加依賴
在 pubspec.yaml 中添加 Provider 的依賴。
實際添加請參考:https://pub.flutter-io.cn/packages/provider#-installing-tab-
由于版本沖突添加失敗請參考: https://juejin.im/post/5b8958d351882542b03e6d57
3.2 第二步:創(chuàng)建數(shù)據(jù) Model
這里的 Model 實際上就是我們的狀態(tài),它不僅儲存了我們的數(shù)據(jù)模型,而且還包含了更改數(shù)據(jù)的方法,并暴露出它想要暴露出的數(shù)據(jù)。
import?'package:flutter/material.dart';class?CounterModel?with?ChangeNotifier?{int?_count?=?0;int?get?value?=>?_count;void?increment()?{_count++;notifyListeners();} }這個類意圖很清晰,數(shù)據(jù)就是一個 int 類型的?_count,下劃線代表私有。通過?get value?把?_count?值暴露出來。并提供?increment?方法用于更改數(shù)據(jù)。
這里使用了 mixin 混入了?ChangeNotifier,這個類能夠幫助我們自動管理所有聽眾。
當調(diào)用?notifyListeners()?時,它會通知所有聽眾進行刷新。
3.3 第三步:創(chuàng)建頂層共享數(shù)據(jù)
我們在 main 方法中初始化全局數(shù)據(jù):剛才編寫的 CounterModel 以及 textSize。為了要在不同頁面共享這個數(shù)據(jù),我們就需要將其放入頂層節(jié)點(MaterialApp 之上)進行保存。
void?main()?{final?counter?=?CounterModel();final?textSize?=?48;runApp(Provider<int>.value(value:?textSize,child:?ChangeNotifierProvider.value(value:?counter,child:?MyApp(),),),); }通過?Provider<T>.value?能夠管理一個恒定的數(shù)據(jù),并提供給子孫節(jié)點使用。我們只需要將數(shù)據(jù)在其 value 屬性中聲明即可。在這里我們將textSize?傳入。
而?ChangeNotifierProvider<T>.value?不僅能夠提供數(shù)據(jù)供子孫節(jié)點使用,還可以在數(shù)據(jù)改變的時候通知所有聽眾刷新。(通過之前我們說過的?notifyListeners)
此處的?<T>?泛型可省略。但是我建議大家還是進行聲明,這會使你的應(yīng)用更加健壯。
除上述幾個屬性之外?Provider<T>.value?還提供?UpdateShouldNotify?Function,用于控制刷新時機。
typedef UpdateShouldNotify<T> = bool Function(T previous, T current);
我們可以在這里傳入一個方法 (T previous, T current){...},并獲得前后兩個 Model 的實例,然后通過比較兩個 Model 以自定義刷新規(guī)則,這個方法將返回 bool 表示是聽眾否需要刷新。(默認為 previous != current 則刷新。)
為了讓各位思維連貫,我還是在這里放上這個平淡無奇的 MyApp Widget 代碼。
class?MyApp?extends?StatelessWidget?{@overrideWidget?build(BuildContext?context)?{return?MaterialApp(theme:?ThemeData.dark(),home:?FirstScreen(),);} }3.4 第四步:在子頁面中獲取狀態(tài)
在這里我們有兩個頁面,FirstScreen 和 SecondScreen。我們先來看 FirstScreen 的代碼。
3.4.1?Provider.of<T>(context)
class?FirstScreen?extends?StatelessWidget?{@overrideWidget?build(BuildContext?context)?{final?_counter?=?Provider.of<CounterModel>(context);final?textSize?=?Provider.of<int>(context).toDouble();return?Scaffold(appBar:?AppBar(title:?Text('FirstPage'),),body:?Center(child:?Text('Value:?${_counter.value}',style:?TextStyle(fontSize:?textSize),),),floatingActionButton:?FloatingActionButton(onPressed:?()?=>?Navigator.of(context).push(MaterialPageRoute(builder:?(context)?=>?SecondPage())),child:?Icon(Icons.navigate_next),),);} }獲取頂層數(shù)據(jù)最簡單的方法就是?Provider.of<T>(context);
這里的泛型?<T>?指定了獲取?FirstScreen?向上尋找最近的儲存了 T 的祖先節(jié)點的數(shù)據(jù)。我們通過這個方法獲取了頂層的 CounterModel 及 textSize。并在 Text 組件中進行使用。
在 Provider.of(context) 中還有一個 bool 類型的 listen 參數(shù),它代表了是否監(jiān)聽數(shù)據(jù)變化,默認為 true。
floatingActionButton 用來點擊跳轉(zhuǎn)到 SecondScreen 頁面,和我們的主題無關(guān),你可以忽略這部分代碼。
3.4.2 Consumer
看到這里你可能會想,兩個頁面都是獲取頂層狀態(tài),代碼不都一樣嗎。別忙著跳到下一節(jié),我們來看另外一種獲取狀態(tài)的方式,使用它能夠改善應(yīng)用程序性能。
class?SecondPage?extends?StatelessWidget?{@overrideWidget?build(BuildContext?context)?{return?Scaffold(appBar:?AppBar(title:?Text('Second?Page'),),body:?Consumer2<CounterModel,int>(builder:?(context,?CounterModel?counter,?int?textSize,?_)?=>?Center(child:?Text('Value:?${counter.value}',style:?TextStyle(fontSize:?textSize.toDouble(),),),),),floatingActionButton:?Consumer<CounterModel>(builder:?(context,?CounterModel?counter,?child)?=>?FloatingActionButton(onPressed:?counter.increment,child:?child,),child:?Icon(Icons.add),),);} }這里我們要介紹的是第二種方式,使用 Consumer 獲取祖先節(jié)點中的數(shù)據(jù)。
在這個頁面中,我們有兩處使用到了公共 Model。
應(yīng)用中心的文字:使用 CounterModel 在 Text 中展示文字,以及通過 textSize 定義自身的大小。使用到了兩個 Model 中的數(shù)據(jù)。
浮動按鈕:使用 CounterModel 的?increment?方法觸發(fā)計數(shù)器的值增加。使用到了一個 Model。
(1) Single Model Consumer
我們先看 floatingActionButton,使用了一個 Consumer 的情況。
Consumer 使用了?Builder?模式,收到更新通知就會通過 builder 重新構(gòu)建。Consumer<T>?代表了它要獲取哪一個祖先中的 Model。
Consumer 的 builder 實際上就是一個 Function,它接收三個參數(shù)(BuildContext context, T model, Widget child)。
context: context 就是 build 方法傳進來的 BuildContext。
T:T也很簡單,就是獲取到的最近一個祖先節(jié)點中的數(shù)據(jù)模型。
child:它用來構(gòu)建那些與 Model 無關(guān)的部分,在多次運行 builder 中,child 不會進行重建。
然后它會返回一個通過這三個參數(shù)映射的 Widget 用于構(gòu)建自身。
在這個浮動按鈕的例子中,我們通過?Consumer?獲取到了頂層的CounterModel?實例。并在浮動按鈕 onTap 的 callback 中調(diào)用其increment?方法。
而且我們成功抽離出?Consumer?中不變的部分,也就是浮動按鈕中心的Icon?并將其作為 child 參數(shù)傳入 builder 方法中。
(2)Consumer2
現(xiàn)在我們再來看中心的文字部分。這時候你可能會有疑惑了,剛才我們講的 Consumer 獲取的只有一個 Model,而現(xiàn)在 Text 組件不僅需要 CounterModel 用以顯示計數(shù)器,而且還需要獲得 textSize 以調(diào)整字體大小,該怎么做呢?
遇到這種情況你可以使用?Consumer2<A,B>。使用方式基本上和?Consumer<T>?一致,只不過泛型改為了兩個,并且 builder 方法也變成了Function(BuildContext context, A value, B value2, Widget child)。
從源碼里面可以看到,這樣的幫助類最多的是?Consumer6。如果還有個更多的需求,可以直接按照源碼定制你的 Consumer。(感覺并不是很優(yōu)雅)
(3)Provider.of<T>(context) 與 Consumer 的區(qū)別
那么,二者到底有什么差別呢?我們來看 Consumer 的內(nèi)部實現(xiàn)。
@overrideWidget?build(BuildContext?context)?{return?builder(context,Provider.of<T>(context),child,);}可以發(fā)現(xiàn),Consumer?就是通過?Provider.of<T>(context)?來實現(xiàn)的。但是從實現(xiàn)來講?Provider.of<T>(context)?比?Consumer?簡單好用太多,為什么我要使用更加復雜的 Consumer?
實際上?Consumer?非常有用,它的經(jīng)典之處在于能夠在復雜項目中,極大地縮小你的控件刷新范圍。Provider.of<T>(context)?將會把調(diào)用了該方法的 context 作為聽眾,并在?notifyListeners?的時候通知其刷新。
舉個例子來說,我們的 FirstScreen 使用了?Provider.of<T>(context)?來獲取數(shù)據(jù),SecondScreen 則沒有。
你在 FirstScreen 中的 build 方法中添加一個?print('first screen rebuild');
然后在 SecondScreen 中的 build 方法中添加一個?print('second screen rebuild');
點擊第二個頁面的浮動按鈕,那么你會在控制臺看到這句輸出。
3.5 First screen rebuild
首先這證明了?Provider.of<T>(context)?會導致調(diào)用的 context 頁面范圍的刷新。
那么第二個頁面刷新沒有呢? 刷新了,但是只刷新了?Consumer?的部分,甚至連浮動按鈕中的?Icon?的不刷新我們都給控制了。你可以在Consumer?的 builder 方法中驗證,這里不再啰嗦。
假如你在你的應(yīng)用的?頁面級別?的 Widget 中,使用了?Provider.of<T>(context)。會導致什么后果已經(jīng)顯而易見了,每當其狀態(tài)改變的時候,你都會重新刷新整個頁面。雖然你有 Flutter 的自動優(yōu)化算法給你撐腰,但你肯定無法獲得最好的性能。
所以在這里我建議各位盡量使用?Consumer?而不是?Provider.of<T>(context)?獲取頂層數(shù)據(jù)。
以上便是一個最簡單的使用 Provider 的例子。
四、你還需要知道的
4.1 合理選擇使用 Provider 的構(gòu)造方法
在上面這個例子中,我們選擇了使用?XProvider<T>.value?的構(gòu)造方法來創(chuàng)建祖先節(jié)點中的提供者。除了這種方式,我們還可以使用默認構(gòu)造方法。
Provider({Key?key,@required?ValueBuilder<T>?builder,Disposer<T>?dispose,Widget?child,})?:?this._(key:?key,delegate:?BuilderStateDelegate<T>(builder,?dispose:?dispose),updateShouldNotify:?null,child:?child,);常規(guī)的 key/child 屬性我們不在這里展開講解了。先來看這個看上去相對教復雜一點的 builder。
4.1.1 ValueBuilder
相比起?.value?構(gòu)造方式中直接傳入一個 value 就 ok,這里的 builder 要求我們傳入一個 ValueBuilder。這是什么東西呢?
typedef ValueBuilder<T> = T Function(BuildContext context);
通過源碼可以看到,ValueBuilder 其實很簡單,就是傳入一個 Function 返回一個數(shù)據(jù)而已。在上面這個例子中,你可以替換成這樣。
由于是 Builder 模式,這里默認需要傳入 context,實際上我們的 Model(textSize)與 context 并沒有關(guān)系,所以你完全可以這樣寫。
4.1.2 Disposer
現(xiàn)在我們知道了 builder,那這個 dispose 方法又用來做什么的呢。實際上這才是 Provider 的點睛之筆。
typedef Disposer<T> = void Function(BuildContext context, T value);
dispose 屬性需要一個?Disposer<T>,而這個其實也是一個回調(diào)。
如果你之前使用過 BLoC 的話,相信你肯定遇到過一個頭疼的問題。我應(yīng)該在什么時候釋放資源呢? BloC 使用了觀察者模式,它旨在替代 StatefulWidget。然而大量的流使用完畢之后必須 close 掉,以釋放資源。
然而 Stateless Widget 并沒有給我們類似于 dispose 之類的方法,這便是 BLoC 的硬傷。你不得不為了釋放資源而使用 StatefulWidget,這與我們的本意相違。而 Provider 則為我們解決了這一點。
當 Provider 所在節(jié)點被移除的時候,它就會啟動?Disposer<T>,然后我們便可以在這里釋放資源。
舉個例子,假如我們有這樣一個 BLoC。
class?ValidatorBLoC?{StreamController<String>?_validator?=?StreamController<String>.broadcast();get?validator?=>?_validator.stream;validateAccount(String?text)?{//Processing?verification?text?...}dispose()?{_validator.close();} }這時候我們想要在某個頁面提供這個 BLoC 但是又不想使用 StatefulWidget。這時候我們可以在頁面頂層套上這個 Provider。
Provider(builder:(_)?=>?ValidatorBLoC(),dispose:(_,?ValidatorBLoC?bloc)?=>?bloc.dispose(),} )我們在 dispose 回調(diào)中關(guān)閉不再使用的流,這樣就完美解決了數(shù)據(jù)釋放的問題!
現(xiàn)在我們可以放心的結(jié)合 BLoC 一起使用了,很贊有沒有。但是現(xiàn)在你可能又有疑問了,在使用 Provider 的時候,我應(yīng)該選擇哪種構(gòu)造方法呢。
我的推薦是,簡單模型就選擇?Provider<T>.value,好處是可以精確控制刷新時機。而需要對資源進行釋放處理等復雜模型的時候,Provider()默認構(gòu)造方式絕對是你的最佳選擇。
其他幾種 Provider 也遵循該模式,需要的時候可以自行查看源碼。
4.2 我該使用哪種 Provider
如果你在 Provider 中提供了可監(jiān)聽對象(Listenable 或者 Stream)及其子類的話,那么你會得到下面這個異常警告。
你可以將本文中所使用到的 CounterModel 放入 Provider 進行提供(記得 hot restart 而不是 hot reload),那么你就能看到上面這個 FlutterError 了。
你也可以在 main 方法中通過下面這行代碼來禁用此提示。Provider.debugCheckInvalidValueType = null;
這是由于 Provider 只能提供恒定的數(shù)據(jù),不能通知依賴它的子部件刷新。提示也說的很清楚了,假如你想使用一個會發(fā)生 change 的 Provider,請使用下面的 Provider。
ListenableProvider
ChangeNotifierProvider
ValueListenableProvider
StreamProvider
4.2.1 ListenableProvider / ChangeNotifierProvider
你可能會在這里產(chǎn)生一個疑問,不是說(Listenable 或者 Stream)才不行嗎,為什么我們的 CounterModel 混入的是 ChangeNotifier 但是還是出現(xiàn)了這個 FlutterError 呢。
class ChangeNotifier implements Listenable
我們再來看上面的這幾個 Provider 有什么異同。先關(guān)注ListenableProvider / ChangeNotifierProvider?這兩個類。
ListenableProvider 提供(provide)的對象是繼承了 Listenable 抽象類的子類。由于無法混入,所以通過繼承來獲得 Listenable 的能力,同時必須實現(xiàn)其?addListener / removeListener?方法,手動管理收聽者。顯然,這樣太過復雜,我們通常都不需要這樣做。
而混入了?ChangeNotifier?的類自動幫我們實現(xiàn)了聽眾管理,所以 ListenableProvider 同樣也可以接收混入了 ChangeNotifier 的類。
ChangeNotifierProvider 則更為簡單,它能夠?qū)ψ庸?jié)點提供一個?繼承/混入/實現(xiàn)?了 ChangeNotifier 的類。通常我們只需要在 Model 中?with ChangeNotifier?,然后在需要刷新狀態(tài)的時候調(diào)用?notifyListeners?即可。
那么?ChangeNotifierProvider?和?ListenableProvider?究竟區(qū)別在哪呢,ListenableProvider?不是也可以提供(provide)混入了 ChangeNotifier 的 Model 嗎。
還是那個你需要思考的問題。你在這里的 Model 究竟是一個簡單模型還是復雜模型。這是因為 ChangeNotifierProvider 會在你需要的時候,自動調(diào)用其 _disposer 方法。
static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose();
我們可以在 Model 中重寫 ChangeNotifier 的 dispose 方法,來釋放其資源。這對于復雜 Model 的情況下十分有用。
4.2.2 ValueListenableProvider
現(xiàn)在你應(yīng)該已經(jīng)十分清楚?ListenableProvider / ChangeNotifierProvider?的區(qū)別了。下面我們來看 ValueListenableProvider。
ValueListenableProvider 用于提供實現(xiàn)了?繼承/混入/實現(xiàn)?了 ValueListenable 的 Model。它實際上是專門用于處理只有一個單一變化數(shù)據(jù)的 ChangeNotifier。
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>
通過 ValueListenable 處理的類不再需要數(shù)據(jù)更新的時候調(diào)用notifyListeners。
好了,終于只剩下最后一個StreamProvider?了。
4.2.3?StreamProvider
StreamProvider?專門用作提供(provide)一條 Single Stream。我在這里僅對其核心屬性進行講解。
T initialData:你可以通過這個屬性聲明這條流的初始值。
ErrorBuilder?catchError:這個屬性用來捕獲流中的 error。在這條流 addError 了之后,你會能夠通過?T Function(BuildContext context, Object error)?回調(diào)來處理這個異常數(shù)據(jù)。實際開發(fā)中它非常有用。
updateShouldNotify:和之前的回調(diào)一樣,這里不再贅述。
除了這三個構(gòu)造方法都有的屬性以外,StreamProvider 還有三種不同的構(gòu)造方法。
StreamProvider(…):默認構(gòu)造方法用作創(chuàng)建一個 Stream 并收聽它。
StreamProvider.controller(…):通過 builder 方式創(chuàng)建一個StreamController。并且在 StreamProvider 被移除時,自動釋放 StreamController。
StreamProvider.value(…):監(jiān)聽一個已有的 Stream 并將其 value 提供給子孫節(jié)點。
除了上面這五種已經(jīng)提到過的 Provider,還有一種 FutureProvider,它提供了一個 Future 給其子孫節(jié)點,并在 Future 完成時,通知依賴的子孫節(jié)點進行刷新,這里不再詳細介紹,需要的話自行查看 api 文檔。
4.3 優(yōu)雅地處理多個 Provider
在我們之前的例子中,我們使用了嵌套的方式來組合多個 Provider,但是這樣看上去有些傻。這時候我們就可以使用一個非常 sweet 的組件 ——?MultiProvider。
這時候我們剛才那個例子就可以改成這樣。
void?main()?{final?counter?=?CounterModel();final?textSize?=?48;runApp(MultiProvider(providers:?[Provider.value(value:?textSize),ChangeNotifierProvider.value(value:?counter)],child:?MyApp(),),); }可以看到我們的代碼意圖清晰很多,而且與剛才的嵌套做法完全等價。
五、Tips
5.1 保證 build 方法無副作用
Build 無副作用也通常被人叫做,build 保持 pure,二者是同一個意思。
通常我們經(jīng)常會看到,為了獲取頂層數(shù)據(jù)我們會在 build 方法中調(diào)用 XXX.of(context) 方法。你必須非常小心,你的 build 函數(shù)不應(yīng)該產(chǎn)生任何副作用,包括新的對象(Widget 以外),請求網(wǎng)絡(luò),或作出一個映射視圖以外的操作等。
這是因為,你的根本無法控制什么時候你的 build 函數(shù)將會被調(diào)用。(我可以說隨時)每當你的 build 函數(shù)被調(diào)用,那么都會產(chǎn)生一個副作用。這將會發(fā)生非常恐怖的事情。
我這樣說你肯定會感到比較抽象,我們來舉一個例子。
假如你有一個?ArticleModel?這個 Model 的作用是?通過網(wǎng)絡(luò)?獲取一頁 List 數(shù)據(jù),并用 ListView 顯示在頁面上。
這時候,我們假設(shè)你在 build 函數(shù)中做了下面這些事情。
@overrideWidget?build(BuildContext?context)?{final?articleModel?=?Provider.of<ArticleModel>(context);mainCategoryModel.getPage();?//?By?requesting?data?from?the?serverreturn?XWidget(...);}我們在 build 函數(shù)中獲得了祖先節(jié)點中的 articleModel,隨后調(diào)用了 getPage 方法來獲取第一頁的數(shù)據(jù)。
這時候會發(fā)生什么事情呢?當我們請求成功獲得了結(jié)果的時候,根據(jù)之前我們已經(jīng)介紹過的,調(diào)用了?Provider.of<T>(context);?之后數(shù)據(jù)更改會重新運行其 build。這樣 getPage 就又被執(zhí)行了一次。
而你的 Model 中每次請求 getPage 都會導致 Model 中保存的當前請求頁自增(第一次請求第一頁的數(shù)據(jù),第二次請求第二頁的數(shù)據(jù)以此類推),那么每次 build 都會導致新的一次數(shù)據(jù)請求,并在新的數(shù)據(jù) get 的時候請求下一頁的數(shù)據(jù)。你的服務(wù)器掛掉那是遲早的事情。
所以你應(yīng)該嚴格遵守這項原則,否則會導致一系列糟糕的后果。
那么怎么解決數(shù)據(jù)初始化這個問題呢,請看 Q&A 部分。
5.2 不要所有狀態(tài)都放在全局
第二個小貼士是不要把你的所有狀態(tài)都放在頂層。開發(fā)者為了圖方便省事,再接觸了狀態(tài)管理之后經(jīng)常喜歡把所有東西都放在頂層 MaterialApp 之上。這樣看上去就很方便共享數(shù)據(jù)了,我要數(shù)據(jù)就直接去獲取。
不要這么做。嚴格區(qū)分你的全局數(shù)據(jù)與局部數(shù)據(jù),資源不用了就要釋放!否則將會一定程度上影響你的應(yīng)用性能。
5.3 盡量在 Model 中使用私有變量“_”
這可能是我們每個人在新手階段都會出現(xiàn)的疑問。為什么要用私有變量呢,我在任何地方都能夠操作成員不是很方便嗎。
一個應(yīng)用需要大量開發(fā)人員參與,你寫的代碼也許在幾個月之后被另外一個開發(fā)看到了,這時候假如你的變量沒有被保護的話,也許同樣是讓 count++,他會用 countController.sink.add(++_count) 這種原始方法,而不是調(diào)用你已經(jīng)封裝好了的 increment 方法。
雖然兩種方式的效果完全一樣,但是第一種方式將會讓我們的業(yè)務(wù)邏輯零散的混入其他代碼中。久而久之項目中就會大量充斥著這些垃圾代碼增加項目代碼耦合程度,非常不利于代碼的維護以及閱讀。
所以,請務(wù)必使用私有變量保護你的 Model。
5.4 控制你的刷新范圍
在 Flutter 中,組合大于繼承的特性隨處可見。常見的 Widget 實際上都是由更小的 Widget 組合而成,直到基本組件為止。為了使我們的應(yīng)用擁有更高的性能,控制 Widget 的刷新范圍便顯得至關(guān)重要。
我們已經(jīng)通過前面的介紹了解到了,在 Provider 中獲取 Model 的方式會影響刷新范圍。所有,請盡量使用 Consumer 來獲取祖先 Model,以維持最小刷新范圍。
在不需要時刻監(jiān)聽狀態(tài)變化的類中可以通過?Provider<T>.of(context, listen: false);?取消監(jiān)聽, 也是提升刷新效率的方式之一。
六、Q&A
在這里對一些大家可能會有疑問的常見問題做一個回答,如果你還有這之外的疑問的話,歡迎在下方評論區(qū)一起討論。
6.1 Provider 是如何做到狀態(tài)共享的
這個問題實際上得分兩步。
6.1.1 獲取頂層數(shù)據(jù)
實際上在祖先節(jié)點中共享數(shù)據(jù)都是通過系統(tǒng)的 InheritedWidget 進行實現(xiàn)的。
Provider 也不例外,在所有 Provider 的 build 方法中,返回了一個 InheritedProvider。
class InheritedProvider<T> extends InheritedWidget
Flutter 通過在每個 Element 上維護一個?InheritedWidget?哈希表來向下傳遞 Element 樹中的信息。通常情況下,多個InheritedWidget?時改變。
所以尋找祖先節(jié)點的時間復雜度為 O(1) !
6.1.2 通知刷新
通知刷新這一步實際上在講各種 Provider 的時候已經(jīng)講過了,其實就是使用了 Listener 模式。Model 中維護了一堆聽眾,每次調(diào)用Provider.of(context)的時候會進行注冊 ,然后 notifiedListener 通知所有聽眾刷新。
6.2 為什么全局狀態(tài)需要放在頂層 MaterialApp 之上
這個問題需要結(jié)合 Navigator 以及 BuildContext 來回答。由于 Flutter 本質(zhì)上是一個單頁面應(yīng)用程序,所以必須放在 Navigator 的 Element 之上才能夠在全局共享數(shù)據(jù)。
6.3 我應(yīng)該在哪里進行數(shù)據(jù)初始化
對于數(shù)據(jù)初始化這個問題,我簡單將其分為全局數(shù)據(jù)初始化與單頁面數(shù)據(jù)初始化兩種情況。
6.3.1 全局數(shù)據(jù)
當我們需要獲取全局頂層數(shù)據(jù)(就像之前 CounterApp 例子一樣)并需要做一些會產(chǎn)生額外結(jié)果的時候,main 函數(shù)是一個很好的選擇。
我們可以在 main 方法中創(chuàng)建 Model 并進行初始化的工作,這樣就只會執(zhí)行一次。
6.3.2 單頁面
如果我們的數(shù)據(jù)只是在這個頁面中需要使用,那么你有這兩種方式可以選擇。
(1)StatefulWidget
第一種是頁面級別還是使用 StatefulWidget,然后在其 State 的 didChangeDependence 生命周期中,做這些會產(chǎn)生額外結(jié)果的動作的事。由于 State 是長聲明周期對象,在其存在期間,didChangeDependence 只會在創(chuàng)建的時候執(zhí)行一次。
class?FirstScreen?extends?StatefulWidget?{···}class?_FirstScreenState?extends?State<FirstScreen>?{CounterModel?_counter;double?_textSize;@overridevoid?didChangeDependencies()?{super.didChangeDependencies();_counter?=?Provider.of<CounterModel>(context);_textSize?=?Provider.of<int>(context).toDouble();_counter.increment();}...}(2)cascade
你也可以在使用 dart 的級連語法?..do()?直接在頁面的 StatelessWidget 成員變量聲明時進行初始化。
class?FirstScreen?extends?StatelessWidget?{CounterModel?_counter?=?CounterModel()..increment();double?_textSize?=?48;... }使用這種方式需要注意,當這個 StatelessWidget 重新運行 build 的時候,狀態(tài)會丟失。這種情況在 TabBarView 中的子頁面切換過程中就可能會出現(xiàn)。
所以建議還是使用第一種,在 State 中初始化數(shù)據(jù)。
6.4 我需要擔心性能問題嗎
是的,你需要隨時注意應(yīng)用性能是否會因為一些不當操作而降低。雖然 Flutter 可以在不做大量優(yōu)化的情況下媲美原生應(yīng)用的體驗。然而當我們不遵守其行為規(guī)范的時候,會出現(xiàn)這樣的情況。性能會因為你的各種不當操作而變得很糟糕。
然而 Provider 僅僅是對 InheritedWidget 的一個升級,你不必擔心引入 Provider 會對應(yīng)用造成性能問題。但是在使用過程中我有下面三個建議,以避免進入性能陷阱:
控制 Widget 刷新范圍;
保持 build 方法 pure;
必要時,通過重寫 UpdateShouldNotify?進行性能優(yōu)化。
6.5 為什么選擇 Provider
Provider 不僅做到了提供數(shù)據(jù),而且它擁有著一套完整的解決方案,覆蓋了你會遇到的絕大多數(shù)情況。就連 BLoC 未解決的那個棘手的 dispose 問題,和 ScopedModel 的侵入性問題,它也都解決了。
然而它就是完美的嗎,并不是,至少現(xiàn)在來說。Flutter Widget 構(gòu)建模式很容易在 UI 層面上組件化,但是僅僅使用 Provider,Model 和 View 之間還是容易產(chǎn)生依賴。我們只有通過手動將 Model 轉(zhuǎn)化為 ViewModel 這樣才能消除掉依賴關(guān)系,所以假如各位有組件化的需求,還需要另外處理。
不過對于大多數(shù)情況來說,Provider 足以優(yōu)秀,它能夠讓你開發(fā)出?簡單、高性能、層次清晰、高可擴展性?的應(yīng)用。
6.6 我應(yīng)該如何選擇狀態(tài)管理
介紹了這么多狀態(tài)管理,你可能會發(fā)現(xiàn),一些狀態(tài)管理之間職責并不沖突。例如 BLoC 可以結(jié)合 RxDart 庫變得很強大,很好用。而 BLoC 也可以結(jié)合 Provider / ScopedModel 一起使用。那我應(yīng)該選擇哪種狀態(tài)管理方式呢。
我的看法是,沒有最好的,只有最合適的。根據(jù)你的業(yè)務(wù)來選擇最合適的狀態(tài)管理方式,面對不同復雜度的業(yè)務(wù),往往會得出完全不同的結(jié)論。
我建議遵守以下幾點:
1. 使用狀態(tài)管理的目的是為了讓編寫代碼變得更簡單,任何會增加你的應(yīng)用復雜度的狀態(tài)管理,統(tǒng)統(tǒng)都不要用。
2. 選擇自己能夠 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 這些狀態(tài)管理方式都有一定上手難度,不要選自己無法理解的狀態(tài)管理方式。
3. 在做最終決定之前,敲一敲 demo,真正感受各個狀態(tài)管理方式給你帶來的 好處/壞處 然后再做你的決定。
希望能夠幫助到你。
七、源碼淺析
7.1 Flutter 中的 Builder 模式
在 Provider 中,各種 Provider 的原始構(gòu)造方法都有一個 builder 參數(shù),這里一般就用?(_) => XXXModel()?就行了。感覺有點多次一舉,為什么不能像?.value()?構(gòu)造方法那樣簡潔呢。
實際上,Provider 為了幫我們管理 Model,使用到了 delegation pattern。
builder 聲明的 ValueBuilder 最終被傳入代理類?BuilderStateDelegate?/SingleValueDelegate。 然后通過代理類才實現(xiàn)的 Model 聲明周期管理。
class?BuilderStateDelegate<T>?extends?ValueStateDelegate<T>?{BuilderStateDelegate(this._builder,?{Disposer<T>?dispose}):?assert(_builder?!=?null),_dispose?=?dispose;final?ValueBuilder<T>?_builder;final?Disposer<T>?_dispose;T?_value;@overrideT?get?value?=>?_value;@overridevoid?initDelegate()?{super.initDelegate();_value?=?_builder(context);}@overridevoid?didUpdateDelegate(BuilderStateDelegate<T>?old)?{super.didUpdateDelegate(old);_value?=?old.value;}@overridevoid?dispose()?{_dispose?.call(context,?value);super.dispose();} }這里就僅放 BuilderStateDelegate,其余的請自行查看源碼。
7.2 如何實現(xiàn) MultiProvider
Widget?build(BuildContext?context)?{var?tree?=?child;for?(final?provider?in?providers.reversed)?{tree?=?provider.cloneWithChild(tree);}return?tree;}MultiProvider 實際上就是通過每一個 provider 都實現(xiàn)了的 cloneWithChild 方法,用循環(huán)把自己一層一層包裹起來。
MultiProvider(providers:[AProvider,BProvider,CProvider,],child:?child, )等價于:
AProvider(child:?BProvider(child:?CProvider(child:?child,),), )以上,是本人在狀態(tài)管理方面遇到的心得的總結(jié),希望能夠給各位提供參考。
總結(jié)
以上是生活随笔為你收集整理的Flutter 状态管理指南之 Provider的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux软件包管理— rpm软件包查询
- 下一篇: 2021考研总结