老生常談的幾個問題
????一.為什么需要狀態(tài)管理?
? ? 二.什么是Provider?
? ? 三.Provider如何使用(最關(guān)心的問題)?
? ? 四.Provider 是如何做到狀態(tài)共享的?
? ? 五.為什么選擇 Provider唉工?? ?
? ? 回答第一個問題法严,先看兩張圖?
? ??
這個圖相信大家也很常見寄月,就是創(chuàng)建flutter工程的時的默認demo,頁面也很簡單視圖中間有個text文本手负,點擊按鈕時文本數(shù)字自增,單個頁面狀態(tài)很可控,沒有setState無法實現(xiàn)的功能茬祷。
如果不是單個頁面呢清焕,是多個頁面,其中一些頁面之間相互關(guān)聯(lián)祭犯,正常開發(fā)中不可能遇到每個頁面獨立互不影響的秸妥,如果有效的管理這些多頁面共用的狀態(tài)成為一個頭痛的問題,也解釋了為什么要多界面狀態(tài)管理沃粗。
現(xiàn)在回答第二個問題粥惧,Provider 從名字上就很容易理解,它就是用于提供數(shù)據(jù)最盅,無論是在單個頁面還是在整個?app 都有它自己的解決方案突雪,我們可以很方便的管理狀態(tài)。
如何使用Provider
? ??第一步:添加依賴
????在pubspec.yaml中添加Provider的依賴(provider: ^3.1.0)
? ??第二步:創(chuàng)建數(shù)據(jù) Model
? ??這里的 Model 實際上就是我們的狀態(tài)涡贱,它不僅儲存了我們的數(shù)據(jù)模型咏删,而且還包含了更改數(shù)據(jù)的方法,并暴露出它想要暴露出的數(shù)據(jù)问词,如圖三
? ??
這個類意圖非常清晰督函,我們的數(shù)據(jù)就是一個 int 類型的?_count,下劃線代表私有。通過?get value?把?_count?值暴露出來辰狡。并提供?increment?方法用于更改數(shù)據(jù)锋叨。
這里使用了 mixin 混入了?ChangeNotifier,這個類能夠幫駐我們自動管理所有聽眾搓译。當(dāng)調(diào)用?notifyListeners()?時悲柱,它會通知所有聽眾進行刷新。
第三步:創(chuàng)建頂層共享數(shù)據(jù)
? ??我們在 main 方法中初始化全局數(shù)據(jù)些己,如圖4,這個是在我已有項目中建的所以有一些別的引用類.
標(biāo)紅的代碼意思嘿般,runApp是優(yōu)化的后的代碼段标,通過?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 則刷新。
第四步:在子頁面中獲取狀態(tài)
獲取頂層數(shù)據(jù)最簡單的方法就是?Provider.of<T>(context);?這里的范型?<T>?指定了獲取?FirstScreen?向上尋找最近的儲存了 T 的祖先節(jié)點的數(shù)據(jù)员辩。
我們通過這個方法獲取了頂層的 CounterModel 及 textSize盒粮。并在 Text 組件中進行使用,如圖5使用起來也容易奠滑,一般在無狀態(tài)組件中放入build中丹皱。
在有狀態(tài)組件中最好使用,有些界面一進入會行網(wǎng)絡(luò)請求养叛,如果放入圖五的build中會無限調(diào)用种呐,所以根據(jù)自己的需求來決定調(diào)用方式。
如果只是應(yīng)用的話上面幾個介紹就可以使用Provider了弃甥。
下面講一下提升的方案爽室,比如我只是點擊觸發(fā)了一個按鈕,不希望當(dāng)前界面重新繪制,要怎么辦呢阔墩?
圖五中嘿架,floatingActionButton出現(xiàn)了Consumer,如果你細心看的話發(fā)會現(xiàn)啸箫。
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 方法中峰搪。
可以發(fā)現(xiàn),Consumer?就是通過?Provider.of<T>(context)?來實現(xiàn)的庵楷。但是從實現(xiàn)來講?Provider.of<T>(context)?比?Consumer?簡單好用太多罢艾,為啥我要搞得那么復(fù)雜捏。
實際上?Consumer?非常有用尽纽,它的經(jīng)典之處在于能夠在復(fù)雜項目中咐蚯,極大地縮小你的控件刷新范圍。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');
點擊第二個頁面的浮動按鈕,那么你會在控制臺看到這句輸出危尿。
first screen rebuild
首先這證明了?Provider.of<T>(context)?會導(dǎo)致調(diào)用的 context 頁面范圍的刷新呐萌。
那么第二個頁面刷新沒有呢? 刷新了谊娇,但是只刷新了?Consumer?的部分肺孤,甚至連浮動按鈕中的?Icon?的不刷新我們都給控制了。你可以在?Consumer?的 builder 方法中驗證,這里不再啰嗦
假如你在你的應(yīng)用的?頁面級別?的 Widget 中赠堵,使用了?Provider.of<T>(context)小渊。會導(dǎo)致什么后果已經(jīng)顯而易見了,每當(dāng)其狀態(tài)改變的時候茫叭,你都會重新刷新整個頁面酬屉。雖然你有 Flutter 的自動優(yōu)化算法給你撐腰,但你肯定無法獲得最好的性能揍愁。
所以在這里我建議各位盡量使用?Consumer?而不是?Provider.of<T>(context)?獲取頂層數(shù)據(jù)呐萨。
以上便是一個最簡單的使用 Provider 的例子。Consumer最多支持取六個Modal不能無限所有Modal,通常情況下是夠用了吗垮。
控制你的刷新范圍
在 Flutter 中垛吗,組合大于繼承的特性隨處可見。常見的 Widget 實際上都是由更小的 Widget 組合而成烁登,直到基本組件為止。為了使我們的應(yīng)用擁有更高的性能蔚舀,控制 Widget 的刷新范圍便顯得至關(guān)重要饵沧。
我們已經(jīng)通過前面的介紹了解到了,在 Provider 中獲取 Model 的方式會影響刷新范圍赌躺。所有狼牺,請盡量使用 Consumer 來獲取祖先 Model,以維持最小刷新范圍礼患。
Provider 是如何做到狀態(tài)共享的
這個問題實際上得分兩步是钥。
獲取頂層數(shù)據(jù)
實際上在祖先節(jié)點中共享數(shù)據(jù)這件事我們已經(jīng)在之前的文章中接觸過很多次了,都是通過系統(tǒng)的 InheritedWidget 進行實現(xiàn)的缅叠。
Provider 也不例外悄泥,在所有 Provider 的 build 方法中,返回了一個 InheritedProvider肤粱。
class InheritedProvider<T> extends InheritedWidget
Flutter 通過在每個 Element 上維護一個?InheritedWidget?哈希表來向下傳遞 Element 樹中的信息弹囚。通常情況下,多個 Element 引用相同的哈希表领曼,并且該表僅在 Element 引入新的?InheritedWidget?時改變鸥鹉。
通知刷新
通知刷新這一步實際上在講各種 Provider 的時候已經(jīng)講過了,其實就是使用了 Listener 模式庶骄。Model 中維護了一堆聽眾毁渗,然后 notifiedListener 通知刷新。
為什么選擇 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)用虐骑。
我應(yīng)該如何選擇狀態(tài)管理
介紹了這么多狀態(tài)管理准验,你可能會發(fā)現(xiàn),一些狀態(tài)管理之間職責(zé)并不沖突廷没。例如 BLoC 可以結(jié)合 RxDart 庫變得很強大糊饱,很好用。而 BLoC 也可以結(jié)合 Provider / ScopedModel 一起使用颠黎。那我應(yīng)該選擇哪種狀態(tài)管理方式呢另锋。
我的建議是遵守以下幾點:
使用狀態(tài)管理的目的是為了讓編寫代碼變得更簡單,任何會增加你的應(yīng)用復(fù)雜度的狀態(tài)管理狭归,統(tǒng)統(tǒng)都不要用夭坪。
選擇自己能夠 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 這些狀態(tài)管理方式都有一定上手難度唉铜,不要選自己無法理解的狀態(tài)管理方式台舱。
在做最終決定之前,敲一敲 demo潭流,真正感受各個狀態(tài)管理方式給你帶來的 好處/壞處 然后再做你的決定竞惋。
附上我的github學(xué)習(xí)地址:FlutterStudy,希望能幫到一起前行的你,有問題提issue灰嫉。