1.狀态管理原則
StatefulWidget
的狀态應該被誰管理?Widget本身?父 Widget ?都會?還是另一個對象?答案是取決于實際情況!以下是管理狀态的最常見的方法:
- Widget 管理自己的狀态。setState
- Widget 管理子 Widget 狀态。
- 混合管理(父 Widget 和子 Widget 都管理狀态)。
- 全局狀态管理 (三方)
如何決定使用哪種管理方法?下面是官方給出的一些原則可以幫助你做決定:
- 如果狀态是使用者資料,如複選框的選中狀态、滑塊的位置,則該狀态最好由父 Widget 管理。
- 如果狀态是有關界面外觀效果的,例如顔色、動畫,那麼狀态最好由 Widget 本身來管理。
- 如果某一個狀态是不同 Widget 共享的則最好由它們共同的父 Widget 管理。
一般的原則是:如果狀态是元件私有的,則應該由元件自己管理;如果狀态要跨元件共享,則該狀态應該由各個元件共同的父元素來管理。對于元件私有的狀态管理很好了解,但對于跨元件共享的狀态,管理的方式就比較多了。
2.官方狀态管理方式
2.1全局事件總線EventBus
它是一個觀察者模式的實作,通過它就可以實作跨元件狀态同步:狀态持有方(釋出者)負責更新、釋出狀态,狀态使用方(觀察者)監聽狀态改變事件來執行一些操作。

我們可以發現,通過觀察者模式來實作跨元件狀态共享有一些明顯的缺點:
- 必須顯式定義各種事件,不好管理。
- 訂閱者必須需顯式注冊狀态改變回調,也必須在元件銷毀時手動去解綁回調以避免記憶體洩露。
2.2InheritedWidget
比如我們在應用的根 widget 中通過
InheritedWidget
共享了一個資料,那麼我們便可以在任意子widget 中來擷取該共享的資料!
當
InheritedWidget
資料發生變化時,可以自動更新依賴的子孫元件!利用這個特性,我們可以将需要跨元件共享的狀态儲存在
InheritedWidget
中,然後在子元件中引用
InheritedWidget
用法: •自定義Widget繼承自lnheritedWidget, 并且自定義static of方法,用于child擷取目前執行個體 •實作 Inheritedwidtet 類中的 updateShoulaNotify 方法,用于傳回update條件; 當資料變化時,調用自定義widget的State類中的setState(方法,觸發整棵Inheritedwldget tree的更新
定義一個共享資料的InheritedWidget,需要繼承自InheritedWidget
class HYCounterWidget extends InheritedWidget {
// 1.要共享的資料
final int counter;
// 2.定義構造方法
HYCounterWidget({this.counter, Widget child}): super(child: child);
// 3.擷取元件最近的目前InheritedWidget
static HYCounterWidget of(BuildContext context) {
// 沿着Element樹, 去找到最近的HYCounterElement, 從Element中取出Widget對象
return context.dependOnInheritedWidgetOfExactType();
}
// 4.絕對要不要回調State中的didChangeDependencies
// 如果傳回true: 執行依賴當期的InheritedWidget的State中的didChangeDependencies
@override
bool updateShouldNotify(HYCounterWidget oldWidget) {
return oldWidget.counter != counter;
}
}
優點:
- 自動訂閱
InheritedWidget内部會維護一個Widget的Map,當子Widget調用Context#inheritFromWidgetOfExactType時就會自動将子Widget存入Map中,并且将InheritedWidget傳回給子Widget。
- 自動通知
InheritedWidget重建後悔自動觸發InheritElement的Update方法。
缺點:
- 無法分離視圖邏輯和業務邏輯。
- 無法定向通知/指向性通知。
InheritedWidget不會區分Widget是否需要更新的問題,每次更新都會通知所有的子Widget。是以需要配合StreamBuilder來解決問題。
StreamBuilder是Flutter封裝好的監聽Stream資料變化的Widget,本質上是一個
StatefulWidget
,内部通過
Stream.listen()
來監聽傳入的
stream
的變化,當監聽到有變化時就調用
setState()
方法來更新Widget。
關于
stream
的介紹的文章到處都有,别人寫的也很詳細,這裡就不再贅述了。
有了
StreamBuilder
,我們可以在子Widget上通過
StreamBuilder
來監聽InheritedWidget中的
Stream
的資料變化,然後判斷是否需要更新目前的子Widget,這樣就完成了資料的定向通知。
2.3Provider
Provider是目前官方推薦的全局狀态管理工具,由社群作者Remi Rousselet 和 Flutter Team共同編寫。
它對InheritedWidget進行了上層封裝,緻力解決原生setState方案的props臃腫、展示與邏輯耦合問題。
它的原理就是綁定 Inheritedwidget 與依 賴它的子孫元件的依賴關系,并且當 Inheritedwidget 資料發生變化時,可以自動更新依賴的子孫 元件。
Provider将頁面分為業務和視圖兩層,并定義Notifier、Consumer兩個核心概念:
- Notifier負責實作業務邏輯,且在資料更新時發出通知。
- Consumer負責實作界面邏輯,并在資料更新時更新自身,以及使用者互動時調用Notifier方法。
元件間通信依賴公共Notifier父節點完成,其流程為:
- 元件A調用Notifier方法更新資料。
- Notifier節點通知元件B資料更新。
優勢
- 方案涉及概念少,上手成本低。
劣勢
- 資料流分為業務、視圖兩層。項目規模變大時,業務層複雜度容易指數級增長。
- 元件通信方式依賴公共Notifier父節點,資料同步與元件樹結構強耦合,項目維護成本高。
- 元件不可插拔,元件擷取資料依賴Notifier父節點,與元件樹結構強耦合,項目維護成本高。
使用之前,我們需要先引入對它的依賴,截止這篇文章,Provider的最新版本為4.0.4:
dependencies:
provider: ^4.0.4
2.3.1 Provider基本使用
在使用Provider的時候,我們主要關心三個概念:
- ChangeNotifier:真正資料(狀态)存放的地方
- ChangeNotifierProvider:Widget樹中提供資料(狀态)的地方,會在其中建立對應的ChangeNotifier
- Consumer:Widget樹中需要使用資料(狀态)的地方
我們先來完成一個簡單的案例,将官方計數器案例使用Provider來實作:
第一步:建立自己的ChangeNotifier
我們需要一個ChangeNotifier來儲存我們的狀态,是以建立它
- 這裡我們可以使用繼承自ChangeNotifier,也可以使用混入,這取決于機率是否需要繼承自其它的類
- 我們使用一個私有的_counter,并且提供了getter和setter
- 在setter中我們監聽到_counter的改變,就調用notifyListeners方法,通知所有的Consumer進行更新
class CounterProvider extends ChangeNotifier {
int _counter = 100;
int get counter {
return _counter;
}
set counter(int value) {
_counter = value;
notifyListeners();
}
}
第二步:在Widget Tree中插入ChangeNotifierProvider
我們需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以擷取到資料:
- 将ChangeNotifierProvider放到了頂層,這樣友善在整個應用的任何地方可以使用CounterProvider
void main() {
runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
));
}
第三步:在首頁中使用Consumer引入和修改狀态
- 引入位置一:在body中使用Consumer,Consumer需要傳入一個builder回調函數,當資料發生變化時,就會通知依賴資料的Consumer重新調用builder方法來建構;
- 引入位置二:在floatingActionButton中使用Consumer,當點選按鈕時,修改CounterNotifier中的counter資料;
class HYHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("清單測試"),
),
body: Center(
child: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return Text("目前計數:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
}
),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
Consumer的builder方法解析:
- 參數一:context,每個build方法都會有上下文,目的是知道目前樹的位置
- 參數二:ChangeNotifier對應的執行個體,也是我們在builder函數中主要使用的對象
- 參數三:child,目的是進行優化,如果builder下面有一顆龐大的子樹,當模型發生改變的時候,我們并不希望重新build這顆子樹,那麼就可以将這顆子樹放到Consumer的child中,在這裡直接引入即可(注意我案例中的Icon所放的位置)
案例效果
步驟四:建立一個新的頁面,在新的頁面中修改資料
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二個頁面"),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
第二個頁面修改資料
2.3.2. Provider.of的弊端
事實上,因為Provider是基于InheritedWidget,是以我們在使用ChangeNotifier中的資料時,我們可以通過Provider.of的方式來使用,比如下面的代碼:
Text("目前計數:${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
),
我們會發現很明顯上面的代碼會更加簡潔,那麼開發中是否要選擇上面這種方式了?
- 答案是否定的,更多時候我們還是要選擇Consumer的方式。
為什麼呢?因為Consumer在重新整理整個Widget樹時,會盡可能少的rebuild Widget。
方式一:Provider.of的方式完整的代碼:
- 當我們點選了floatingActionButton時,HYHomePage的build方法會被重新調用。
- 這意味着整個HYHomePage的Widget都需要重新build
class HYHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("調用了HYHomePage的build方法");
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("目前計數:${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
)
],
),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
方式二:将Text中的内容采用Consumer的方式修改如下:
- 你會發現HYHomePage的build方法不會被重新調用;
- 設定如果我們有對應的child widget,可以采用上面案例中的方式來組織,性能更高;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
print("調用Consumer的builder");
return Text(
"目前計數:${counterPro.counter}",
style: TextStyle(fontSize: 30, color: Colors.red),
);
}),
2.3.3. Selector的選擇
Consumer是否是最好的選擇呢?并不是,它也會存在弊端
- 比如當點選了floatingActionButton時,我們在代碼的兩處分别列印它們的builder是否會重新調用;
- 我們會發現隻要點選了floatingActionButton,兩個位置都會被重新builder;
- 但是floatingActionButton的位置有重新build的必要嗎?沒有,因為它是否在操作資料,并沒有展示;
- 如何可以做到讓它不要重新build了?使用Selector來代替Consumer
Select的弊端
我們先直接實作代碼,在解釋其中的含義:
floatingActionButton: Selector<CounterProvider, CounterProvider>(
selector: (ctx, provider) => provider,
shouldRebuild: (pre, next) => false,
builder: (ctx, counterPro, child) {
print("floatingActionButton展示的位置builder被調用");
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
Selector和Consumer對比,不同之處主要是三個關鍵點:
- 關鍵點1:泛型參數是兩個
- 泛型參數一:我們這次要使用的Provider
- 泛型參數二:轉換之後的資料類型,比如我這裡轉換之後依然是使用CounterProvider,那麼他們兩個就是一樣的類型
- 關鍵點2:selector回調函數
- 轉換的回調函數,你希望如何進行轉換
- S Function(BuildContext, A) selector
- 我這裡沒有進行轉換,是以直接将A執行個體傳回即可
- 關鍵點3:是否希望重新rebuild
- 這裡也是一個回調函數,我們可以拿到轉換前後的兩個執行個體;
- bool Function(T previous, T next);
- 因為這裡我不希望它重新rebuild,無論資料如何變化,是以這裡我直接return false;
Selector的使用
這個時候,我們重新測試點選floatingActionButton,floatingActionButton中的代碼并不會進行rebuild操作。
是以在某些情況下,我們可以使用Selector來代替Consumer,性能會更高。
2.3.4. MultiProvider
在開發中,我們需要共享的資料肯定不止一個,并且資料之間我們需要組織到一起,是以一個Provider必然是不夠的。
我們在增加一個新的ChangeNotifier
import 'package:flutter/material.dart';
class UserInfo {
String nickname;
int level;
UserInfo(this.nickname, this.level);
}
class UserProvider extends ChangeNotifier {
UserInfo _userInfo = UserInfo("why", 18);
set userInfo(UserInfo info) {
_userInfo = info;
notifyListeners();
}
get userInfo {
return _userInfo;
}
}
如果在開發中我們有多個Provider需要提供應該怎麼做呢?
方式一:多個Provider之間嵌套
- 這樣做有很大的弊端,如果嵌套層級過多不友善維護,擴充性也比較差
runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: ChangeNotifierProvider(
create: (context) => UserProvider(),
child: MyApp()
),
));
方式二:使用MultiProvider
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (ctx) => CounterProvider()),
ChangeNotifierProvider(create: (ctx) => UserProvider()),
],
child: MyApp(),
));
provider自己的想法總結 重要!:
1. 建立自己需要共享的資料
2. 在應用程式的頂層ChangeNotifierProvider
3. 在其它位置使用共享的資料
- Provider.of: 當Provider中的資料發生改變時, Provider.of所在的Widget整個build方法都會重新建構
- Consumer(相對推薦): 當Provider中的資料發生改變時, 執行重新執行Consumer的builder
- Selector:
1.selector方法(作用,對原有的資料進行轉換)
2.shouldRebuild(作用,要不要重新建構)
3.三方狀态管理包
3.1 BLoC
BLoC是Business Logic Component設計模式在Flutter上的實作。它緻力于解耦展示與邏輯分離,提升代碼的可測試性。
整體結構上,BLoC類似多Store的redux并增加了大量響應式程式設計特性,相較于redux而言:
- 多Store解決了業務邏輯集中,複雜度容易指數級增長的問題,加強了不同業務邏輯間的作用域隔離。
- 響應式程式設計特性減少了redux中的模闆代碼。
整體資料流如下圖:
優勢
- 資料流分為全局多Store和視圖兩層。項目規模變大時,合理配置設定多個Store承載的業務邏輯可避免業務層複雜度過高。
- 元件通信方式依賴全局Store,資料同步與元件樹結構解耦,便于項目維護。
- 元件可插拔,元件擷取資料依賴全局Store,與元件樹解耦,便于項目維護。
劣勢
- 缺少局部Store無法實作業務邏輯在元件範圍的自治。
3.2 fish-redux
- page:總頁面,注冊effect,reducer,component,adapter的功能,相關的配置都在此頁面操作
- state:這地方就是我們存放子子產品變量的地方;初始化變量和接受上個頁面參數,也在此處,是個很重要的子產品
- view:主要是我們寫頁面的子產品
- action:這是一個非常重要的子產品,所有的事件都在此處定義和中轉
- effect:相關的業務邏輯,網絡請求等等的“副作用”操作,都可以寫在該子產品
- reducer:該子產品主要是用來更新資料的,也可以寫一些簡單的邏輯或者和資料有關的邏輯操作
- view ---> action(事件定義和中轉) ---> effect (業務邏輯 網絡)---> reducer(更新資料)
優點:
- 資料集中管理,架構自動完成reducer合并。
- 元件分治管理,元件之間以及和容器之間互相隔離。
- View、Reducer、Effect隔離。易于編寫複用。
- 聲明式配置組裝。
- 良好的擴充性。
個人感覺fish_redux的設計适用于複雜的業務場景,加上複雜的目錄結構以及相關概念,不太适合普通的資料不太複雜的業務。
fish_redux使用詳解---看完就會用!
3.3 redux
fish-redux将頁面分為類redux的Store(業務)和Component(視圖)兩層,每個視圖元件同樣是redux标準結構。
資料更新時,視圖元件首先會嘗試在自身Store中進行計算并更新自身,當發現缺少資料或更新将影響其他視圖元件時,則将資料的改動廣播至全局Store處理。
單個視圖元件的資料流如下圖:
優勢
- 資料流分為全局Store、元件Store群組件視圖三層。項目規模變大時,合理配置設定全局Store、元件Store承載的業務邏輯可避免業務層複雜度過高。同時元件Store的設計幫助實作業務邏輯在元件範圍的自治。
- 元件通信方式依賴全局Store,資料同步與元件樹結構解耦,便于項目維護。
- 元件可插拔,元件擷取資料依賴全局Store,與元件樹結構解耦,便于項目維護。
劣勢
- redux模式下模闆代碼多,合理使用的門檻較高。
- 項目規模變大時,單一的全局Store仍有複雜度過高的可能。