Flutter|一文搞懂何謂狀態管理

語言: CN / TW / HK

1.什麼是狀態管理

2.不同的狀態管理分類

3.Flutter中的有狀態元件和無狀態元件

4.Flutter中有哪些可以做到狀態管理

5.為什麼要使用狀態管理

6.常見的狀態管理框架有哪些

7.狀態管理總結&思考

01

什麼是狀態管理

隨著“大前端”概念流行的同時,響應式程式設計的理念也隨之被越來越多的人所瞭解和學習,要了解和學習響應式的程式設計框架就一定離不開狀態管理,客戶端Android、iOS是通過明確的命令式指令去控制我們的UI變化,如setText,而在響應式程式設計下,我們只需要描述好UI和狀態之間的關係,然後專注於狀態的改變就好了,框架會根據狀態的變化來自動更新UI。

總結來說就是:狀態管理就是當某個狀態發生改變的時候,告知使用該狀態的狀態監聽者,讓狀態所監聽的屬性隨知改變,從而達到聯動效果。

02

不同的狀態管理分類

  • 短時狀態Ephemeral state

    某些狀態、或是可以理解為某些資料只需要在當前的Widget中訪問和使用,不需要對這些狀態進行共享訪問,你需要的只是一個StatefulWidget元件,依靠這個StatefulWidget元件自己的State類自己管理即可,不需要使用狀態管理框架去管理這種狀態,這些狀態可以稱之為短時狀態。

    如:官網中的計數器Demo、比如一個PageView元件記錄當前的頁面

  • 應用狀態App state

    某些狀態需要被元件共享訪問,當這個狀態發生變化的時候,其他元件也需要隨之發生聯動的變化,這就是應用狀態。

    舉個例子來說明,比如一個電商App,在商品的詳情頁面,我們把某個商品加入了購物車,那麼商品是否放入購物車這個狀態,就需要被購物車頁面元件所訪問,那麼這個狀態就是應用狀態。

    試想一下,如果再不使用第三方狀態管理框架的情況下,我們可以怎麼實現呢,可以使用InheritedWidget定向的傳遞,可以通過Notification進行通知,可以使用event_bus來進行事件訂閱等等,其實我們所說的狀態管理框架,也是基於上面說的等幾種方式來實現的。

    總結來說,要區分短時狀態還是應用狀態,就看這個狀態需不需要被多個元件進行訪問,當這個狀態一發生變化,其他元件需要隨之發生聯動變化,就是應用狀態,反之,其他元件不需要變化、不受影響,就是短時狀態。

03

Flutter中的有狀態元件和無狀態元件

在Flutter中,元件根據狀態分為,有狀態元件StatefulWidget和無狀態元件StatelessWidget。

StatelessWidget:無狀態的Widget,它無法通過setState設定元件狀態進行重繪,它內的屬性應該被宣告為final,防止改變。

StatefulWidget:有狀態的Widget,建立一個StatefulWidget元件時,它同時建立一個State物件,通過與State關聯可以達到重新整理UI的目的。

State:在Flutter中,Widget和State具有不同的生命週期,Widget是臨時物件,用於構建當前狀態下的應用程式,而State物件在多次呼叫build()之間保持不變,允許它們儲存資訊(狀態)。

State生命週期:

04

Flutter中有哪些可以做到狀態管理

State

常用而且使用最頻繁的一個狀態管理類,它必須結合StatefulWidget一起使用,StreamBuilder繼承自StatefulWidget,同樣是通過setState來管理狀態
State缺點:

  1. 無法做到跨元件共享資料(這個跨是無關聯的,如果是直接的父子關係,我們不認為是跨元件) setState是State的函式,一般我們會將State的子類設定為私有,所以無法做到讓別的元件呼叫State的setState函式來重新整理。

  2. setState會成為維護的難點,因為啥哪哪都是。隨著頁面狀態的增多,你可能在呼叫setState的地方會越來越多,不能統一管理。

  3. 處理資料邏輯和檢視混合在一起,違反程式碼設計原則 比如資料庫的資料取出來setState到Ui上,這樣編寫程式碼,導致狀態和UI耦合在一起,不利於測試,不利於複用。

  4. setState是整個Widget重新構建(而且子Widget也會跟著銷燬重建),如果頁面足夠複雜,就會導致嚴重的效能損耗。建議使用StreamBuilder,原理上也是State,但它做到了子Widget的區域性重新整理,不會導致整個頁面的重建。

InheritedWidget

它的天生特性就是能繫結InheritedWidget與依賴它的子孫元件的依賴關係,並且當InheritedWidget資料發生變化時,可以自動更新依賴的子孫元件!
利用這個特性,我們可以將需要跨元件共享的狀態儲存在InheritedWidget中,然後在子元件中引用InheritedWidget即可。
專門負責Widget樹中資料共享的功能型Widget,如Provider、scoped_model就是基於它開發的。

InheritedWidget缺點:

  1. 每次更新都會通知所有的子Widget,無法定向通知/指向性通知,容易造成不必要的重新整理。

  2. 不支援跨頁面(route)的狀態,意思是跨樹,如果不在一個樹中,我們無法獲取。

  3. 資料是不可變的,必須結合StatefulWidget、ChangeNotifier或者Steam使用。

Notification

它是Flutter中跨層資料共享的一種機制,注意,它不是widget,它提供了dispatch方法,沿著context對應的Element節點向上逐層傳送通知

Notification缺點:

  1. 不支援跨頁面(route)的狀態,準確說不支援NotificationListener同級或者父級Widget的狀態通知。

  2. 本身不支援重新整理UI,需要結合State使用。

  3. 如果結合State,會導致整個UI的重繪,效率底下不科學。

Stream

純Dart的實現,跟Flutter沒什麼關係,扯上關係的就是用StreamBuilder來構建一個Stream通道的Widget,像知名的rxdart、BloC、flutter_redux、fish_redux全都用到了Stream的api。

Stream 缺點:

  1. api生澀,不好理解。

  2. 需要定製化,才能滿足更復雜的場景。

  3. 缺點恰恰是它的優點,保證了足夠靈活,你更可基於它做一個好的設計,滿足當下業務的設計。

05

為什麼要使用狀態管理

對於不需要傳遞的狀態或者不需要共享的狀態,我們不需要進行復雜的狀態管理,單純依靠setState也可以很好的完成我們的需求。

但是隨著產品迭代節奏速度的加快,專案逐漸變得越來越龐大,不同元件之間的資料依賴性越來越高,我們就需要更清晰、明確的處理各個元件之間的資料關係,這時候如果還單單使用setState做狀態處理,我們就很難明確的處理資料的流向,最終可能會導致資料傳遞和巢狀邏輯過於複雜,不便於維護和管理,在出現問題的時候,也會花費大量的時間成本來捋清資料之間的關係。

總的來說,對於跨元件(跨頁面)之間進行資料共享和傳遞,而且需要保持狀態的一致性和可維護性,這就需要我們對狀態進行管理。

06

常見的狀態管理框架有哪些

Provider

  1. Provider是官方文件的例子用的方法. Google 比較推薦的用法. 和BLoC的流式思想相比, Provider是一個觀察者模式, 狀態改變時要notifyListeners().

  2. Provider的實現在內部還是利用了InheritedWidget,允許將有效資訊傳遞到元件樹下的小元件. Provider的好處: dispose指定後會自動被呼叫, 支援MultiProvider.

  3. Provider從名字上就很容易理解,它就是用於提供資料,無論是在單個頁面還是在整個app 都有它自己的解決方案,可以很方便的管理狀態。

  • 常用概念:

  1. ChangeNotifier:系統提供的被觀察者,資料model需要繼承

  2. Provider:訂閱者,只用於資料共享管理,提供給子孫節點使用,UpdateShouldNotify Function,用於控制重新整理時機

  3. ChangeNotifierProvider:訂閱者,不僅能夠提供資料供子孫節點使用,還可以在資料改變的時候通知所有消費者。Model變化後會自動通知ChangeNotifierProvider(訂閱者),ChangeNotifierProvider內部會重新構建InheritedWidget,而依賴該InheritedWidget的子孫Widget就會更新.

  4. MultiProvider:多個訂閱者:實際上就是通過每一個provider都實現了的 cloneWithChild方法把自己一層一層包裹起來。

  5. Consumer:消費者,能夠在複雜專案中,極大地縮小你的控制元件重新整理範圍。最多支援6中model

  6. Selector: 消費者,強化的Consumer,支援過濾重新整理

  • 使用流程:

  1. 新增依賴

  2. 建立資料 Model

  3. 建立頂層共享資料

  4. 頂層Provider包裹

  5. 在子頁面中獲取狀態

  • Provder種類:

  1. Provider:只能提供恆定的資料,不能通知依賴它的子部件重新整理。

  2. ListenableProvider: 提供的物件是繼承了 Listenable 抽象類的子類,必須實現其 addListener / removeListener 方法,通常不需要。

  3. ChangeNotifierProvider: 對子節點提供一個繼承/混入/實現了ChangeNotifier的類,只需要在Model中with ChangeNotifier ,然後在需要重新整理狀態時呼叫 notifyListeners 即可。

  4. ValueListenableProvider: 提供實現了繼承/混入/實現了ValueListenable的Model,實際上是專門用於處理只有一個單一變化資料的ChangeNotifier。

  5. StreamProvider: 專門用作提供(provide)一條 Single Stream。

  6. FutureProvider:提供了一個 Future 給其子孫節點,並在 Future 完成時,通知依賴的子孫節點進行重新整理。

  • 總結:
    本質上:Prvioder通過inheritedElement實現區域性重新整理,通過控制自己實現的Element層來更新UI,通過Element提供的unmount函式回撥dispose,實現選擇性釋放,
    其核心類:InheritedProvider

Provider不僅做到了提供資料,而且它擁有著一套完整的解決方案,覆蓋了你會遇到的絕大多數情況。就連BLoC未解決的那個棘手的dispose問題,和ScopedModel的侵入性問題,它也都解決了。它能夠讓你開發出簡單、高效能、層次清 的應用。

不足之處:Flutter Widget 構建模式很容易在UI層面上元件化,但是僅僅使用Provider,Model和 View之間還是容易產生依賴。只有通過手動將Model轉化為ViewModel這樣才能消除掉依賴關係。

Redux

Redux是一種單向資料流架構,可以輕鬆開發,維護和測試應用程式,也是google推薦的狀態管理方式。

  • 原理

  1. 所有的狀態都儲存在Store裡。這個Store會放在根Widget.

  2. View拿到Store的狀態資料會對映成檢視渲染.

  3. Redux不直接讓view操作資料,通過dispatch一個action通知Reducer,狀態變更

  4. Reducer接收到這個action,根據action狀態,生成新的狀態,並替換在Store的舊狀態.

  5. Store儲存了新的狀態後,就通知所有使用到了這個狀態的View更新(類似setState)。這樣我們就能夠同步不同view中的狀態了.

  • Redux相關概念

  1. State:資料model

  2. Store 倉庫:整個APP的頂層,儲存和管理state

  3. Action 動作:通過發起一個Action來告訴Reducer該更新狀態了

  4. Reducer 還原:根據Action產生新的狀態

  5. StoreProvider: 一個InheritedWidget,內部儲存了一個Store。(資料中心)最頂層必須是 StoreProvider 開始

  6. StoreConnector: 聯結器:需要兩個泛型
    1)一個是我們建立的 State(ReduxState)
    2)一個是 ViewModel,ViewModel決定了converter(轉換函式)那邊的返回值型別
    同時提供了一個StoreStreamListener,本質上是一個StreamBuilder

  7. StoreConverter:轉換器:類似於Selector中的selector,轉換成本Widget想要的資料

  8. StoreStreamListener: 通過監聽自己的Stream來完成檢視的重建。

  9. StoreBuilder:功能同StoreConnector,StoreConnector主要是有個資料轉化的作用,可以對資料先做一些轉化操作再賦值到元件上,StoreBuilder是直接將資料給顯示在元件上

  10. middleware 中介軟體:類似攔截器,作用域位於reducer更新狀態之前,本質上也是一個函式。
    比如當前是新增使用者動作,但是我想在新增使用者這操作的前面再做一步其他的動作(非同步 action ,action 過濾,日誌輸出,異常報告等),這時候就可以使用中介軟體middleware,實現MiddlewareClass該類就行。

  11. 中介軟體的call方法中有個關鍵方法next(),大多數情況需要呼叫,否則中介軟體的鏈條斷了,後面的中介軟體和Reducer就不執行了。

  12. Dispatcher:如何通知狀態更新呢?通過store.dispatch

  • Redux頁面重新整理流程

  • Redux使用流程:

  1. 新增依賴

  2. 建立State

  3. 建立action

  4. 建立reducer

  5. 建立store

  6. 將Store放入頂層

  7. 在子頁面中獲取Store中的state

  8. 發出action

  • 優點:

  1. 自動訂閱

  2. 自動通知

  3. 可以定向通知

  4. 檢視和業務邏輯分離

  • Redux 的缺點:

  1. Redux 核心僅僅關心資料管理,不關心具體什麼場景來使用它,這是它的優點同時也是它的缺點.

  2. 在我們實際使用 Redux 中面臨兩個具體問題.

  3. Redux 的集中和 Component 的分治之間的矛盾.

  4. Redux 的 Reducer 需要一層層手動組裝,帶來的繁瑣性和易錯性.

GetX

GetX是Flutter上的一個輕量且強大的解決方案,包括但不限於:

  1. 高效的狀態管理。

  2. 便捷的路由管理。

  3. 豐富的Api。

  • GetX的三項基本原則:

  1. 效能:GetX專注於效能和最小資源消耗,GetX打包後的apk佔用大小和執行時的記憶體佔用與其他狀態管理外掛不相上下。

  2. 效率:GetX的語法非常便捷,並保持了極高的效能,能極大縮短你的開發時長。

  3. 結構:GetX可以將介面、邏輯、依賴和路由完全解藕,用起來更清爽,邏輯更清晰,程式碼更容易維護。

  • GetX高效的狀態管理:
    之所以說GetX是高效的狀態管理,是因為他不需要堆疊大量的控制、管理程式碼(如Action、middleware、reducer、state),而且不具有侵入性,可以降低業務和檢視間的耦合度。
    在使用上,使用GetX的響應式狀態管理就像使用setState一樣簡單(其實本質就是setState),並且GetX可以做到區域性重新整理。

  • 使用GetX實現一個簡單的登陸功能
    Demo邏輯:HomePage為主頁面,事件跳轉到登入頁面,登入頁面登入成功後關閉頁面,主頁面重新整理Text文案內容。

class HomePage extends StatelessWidget {
//例項化的Controller
final LoginController logic = Get.put(LoginController(), tag: 'login');


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LoginPage'),
),
body: Center(
child: GestureDetector(
onTap: () {
Get.to(LoginPage());
},
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(
child: Obx(
() => Text(logic.loginStatus.value),
),
),
),
),
),
);
}
}

登入頁面

class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//通過find方法,可以找到你已經例項化的Controller
final LoginController loginController = Get.find(tag: 'login');
return Scaffold(
appBar: AppBar(
title: Text('LoginPage'),
),
body: Center(
child: GestureDetector(
onTap: () {
loginController.login();
},
child: Container(
width: 200,
height: 200,
color: Colors.green,
child: Center(child: Text('點我登入')),
),
),
),
);
}
}

controller控制器

class LoginController extends GetxController{
///通過.obs將loginStatus標記為被觀察者
///loginStatus是RxString型別的,不是String型別
var loginStatus = '未登入'.obs;
login() => {
loginStatus.value = '已登入',
Get.back(),
// update()
};
}
  • 通過Demo感受到GetX的優點

  1. 業務、檢視解藕,業務邏輯可以放在Controller中進行處理。

  2. 程式碼簡潔,無需建立大量的控制類。

  3. 區域性重新整理,當被觀察的資料發生變化時,只有觀察者部分會進行重新整理,不會整個頁面進行重新整理。

  4. 相同的方法(如login),如果被觀察的資料沒有發生變化,則不會進行區域性重新整理。

  5. 從此告別StatefulWidget。

  6. 更簡單的實現跨頁面互動事件。

07

狀態管理總結&思考

7.1 如何選擇框架

沒有哪一種框架可以適配所有的情況,也沒有一種框架可以永遠適用.
應該根據業務分析適合哪一種,當業務變化時,程式碼也需要跟著進化,以適配業務的發展.從一開始就介入fish_redux這樣的框架,成本高,難度大,只是為了實現一些簡單的二級,三級頁面,並不是一個好的選擇。

7.2 選型原則

  • 侵入性

  • 擴充套件性

  • 高效能

  • 安全性

  • 駕馭性

  • 易用性

  • 範圍性

所有的框架都有侵入性,你同意嗎?

目前侵入性比較高的代表ScopedModel,如果你選擇的框架只能使用它提供的幾個入口,可以放棄使用它。

高效能:也是很重要的,這個需要明白它的原理,看它到底如何做的管理。
安全性:也很重要,看他資料管理通道是否安全穩定。
駕馭性:你說你都不理解你就敢用,出了問題找誰?如果駕馭不了也不要用。
易用性:大家應該都明白,如果用它一個框架需要N多配置,N多實現,放棄吧,不合適。簡單才是硬道理。
範圍性 :這個特點是flutter中比較明顯的,框架選型一定要考慮框架的適用範圍,到底是適合做區域性管理,還是適合全域性管理,要做一個實際的考量。

7.3 多種狀態管理框架是否可以同時使用?

當然可以,你用了redux,就不允許setstate()了? 顯然不是.如何同時使用不同的框架能滿足你的需求,使你的效能更好,使用更方便,可讀性更強那就使用吧。