????狀態(tài)管理是聲明式編程非常重要的一個概念昔汉,我們在前面介紹過Flutter是聲明式編程的承二,也區(qū)分聲明式編程和命令式編程的區(qū)別刊愚。
????這里馍驯,我們就來系統(tǒng)的學(xué)習一下Flutter聲明式編程中非常重要的狀態(tài)管理
一. 為什么需要狀態(tài)管理?
1.1. 認識狀態(tài)管理
????很多從命令式編程框架(Android或iOS原生開發(fā)者)轉(zhuǎn)成聲明式編程(Flutter群井、Vue状飞、React等)剛開始并不適應(yīng),因為需要一個新的角度來考慮APP的開發(fā)模式书斜。
????Flutter作為一個現(xiàn)代的框架诬辈,是聲明式編程的:
????在編寫一個應(yīng)用的過程中,我們有大量的State需要來進行管理荐吉,而正是對這些State的改變焙糟,來更新界面的刷新:
1.2. 不同狀態(tài)管理分類
1.2.1. 短時狀態(tài)Ephemeral state
某些狀態(tài)只需要在自己的Widget中使用即可
????比如我們之前做的簡單計數(shù)器counter
????比如一個PageView組件記錄當前的頁面
????比如一個動畫記錄當前的進度
????比如一個BottomNavigationBar中當前被選中的tab
這種狀態(tài)我們只需要使用StatefulWidget對應(yīng)的State類自己管理即可,Widget樹中的其它部分并不需要訪問這個狀態(tài)样屠。
這種方式在之前的學(xué)習中穿撮,我們已經(jīng)應(yīng)用過非常多次了。
1.2.2. 應(yīng)用狀態(tài)App state
開發(fā)中也有非常多的狀態(tài)需要在多個部分進行共享
????比如用戶一個個性化選項
????比如用戶的登錄狀態(tài)信息
????比如一個電商應(yīng)用的購物車
????比如一個新聞應(yīng)用的已讀消息或者未讀消息
這種狀態(tài)我們?nèi)绻赪idget之間傳遞來痪欲、傳遞去悦穿,那么是無窮盡的,并且代碼的耦合度會變得非常高业踢,牽一發(fā)而動全身栗柒,無論是代碼編寫質(zhì)量、后期維護知举、可擴展性都非常差瞬沦。
這個時候我們可以選擇全局狀態(tài)管理的方式太伊,來對狀態(tài)進行統(tǒng)一的管理和應(yīng)用。
1.2.3. 如何選擇不同的管理方式
開發(fā)中逛钻,沒有明確的規(guī)則去區(qū)分哪些狀態(tài)是短時狀態(tài)僚焦,哪些狀態(tài)是應(yīng)用狀態(tài)。
????某些短時狀態(tài)可能在之后的開發(fā)維護中需要升級為應(yīng)用狀態(tài)绣的。
但是我們可以簡單遵守下面這幅流程圖的規(guī)則:
針對React使用setState還是Redux中的Store來管理狀態(tài)哪個更好的問題,Redux的issue上欲账,Redux的作者Dan Abramov屡江,它這樣回答的:
The rule of thumb is: Do whatever is less awkward
經(jīng)驗原則就是:選擇能夠減少麻煩的方式。
二. 共享狀態(tài)管理
2.1. InheritedWidget
InheritedWidget和React中的context功能類似赛不,可以實現(xiàn)跨組件數(shù)據(jù)的傳遞惩嘉。
定義一個共享數(shù)據(jù)的InheritedWidget,需要繼承自InheritedWidget
????這里定義了一個of方法踢故,該方法通過context開始去查找祖先的HYDataWidget(可以查看源碼查找過程)
????updateShouldNotify方法是對比新舊HYDataWidget文黎,是否需要對更新相關(guān)依賴的Widget
class HYDataWidget extends InheritedWidget {
? finalint counter;
? HYDataWidget({this.counter, Widget child}): super(child: child);
? static HYDataWidget of(BuildContext context) {
? ? return context.dependOnInheritedWidgetOfExactType();
? }
? @override
? bool updateShouldNotify(HYDataWidget oldWidget) {
? ? returnthis.counter != oldWidget.counter;
? }
}
創(chuàng)建HYDataWidget,并且傳入數(shù)據(jù)(這里點擊按鈕會修改數(shù)據(jù)殿较,并且重新build)
class HYHomePage extends StatefulWidget {
? @override
? _HYHomePageState createState() => _HYHomePageState();
}
class _HYHomePageState extends State<HYHomePage> {
? int data = 100;
? @override
? Widget build(BuildContext context) {
? ? return Scaffold(
? ? ? appBar: AppBar(
? ? ? ? title: Text("InheritedWidget"),
? ? ? ),
? ? ? body: HYDataWidget(
? ? ? ? counter: data,
? ? ? ? child: Center(
? ? ? ? ? child: Column(
? ? ? ? ? ? mainAxisAlignment: MainAxisAlignment.center,
? ? ? ? ? ? children: <Widget>[
? ? ? ? ? ? ? HYShowData()
? ? ? ? ? ? ],
? ? ? ? ? ),
? ? ? ? ),
? ? ? ),
? ? ? floatingActionButton: FloatingActionButton(
? ? ? ? child: Icon(Icons.add),
? ? ? ? onPressed: () {
? ? ? ? ? setState(() {
? ? ? ? ? ? data++;
? ? ? ? ? });
? ? ? ? },
? ? ? ),
? ? );
? }
}
在某個Widget中使用共享的數(shù)據(jù)耸峭,并且監(jiān)聽
2.2. Provider
Provider是目前官方推薦的全局狀態(tài)管理工具,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫淋纲。
使用之前劳闹,我們需要先引入對它的依賴,截止這篇文章洽瞬,Provider的最新版本為4.0.4:
dependencies:
? provider:^4.0.4
2.2.1. Provider的基本使用
在使用Provider的時候本涕,我們主要關(guān)心三個概念:
????ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
????ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
????Consumer:Widget樹中需要使用數(shù)據(jù)(狀態(tài))的地方
我們先來完成一個簡單的案例伙窃,將官方計數(shù)器案例使用Provider來實現(xiàn):
第一步:創(chuàng)建自己的ChangeNotifier
我們需要一個ChangeNotifier來保存我們的狀態(tài)菩颖,所以創(chuàng)建它
????這里我們可以使用繼承自ChangeNotifier,也可以使用混入为障,這取決于概率是否需要繼承自其它的類
????我們使用一個私有的_counter晦闰,并且提供了getter和setter
????在setter中我們監(jiān)聽到_counter的改變,就調(diào)用notifyListeners方法鳍怨,通知所有的Consumer進行更新
class CounterProvider extends ChangeNotifier {
? int _counter = 100;
? intget counter {
? ? return _counter;
? }
? set counter(int value) {
? ? _counter = value;
? ? notifyListeners();
? }
}
第二步:在Widget Tree中插入ChangeNotifierProvider
我們需要在Widget Tree中插入ChangeNotifierProvider鹅髓,以便Consumer可以獲取到數(shù)據(jù):
????將ChangeNotifierProvider放到了頂層,這樣方便在整個應(yīng)用的任何地方可以使用CounterProvider
void main() {
? runApp(ChangeNotifierProvider(
? ? create: (context) => CounterProvider(),
? ? child: MyApp(),
? ));
}
第三步:在首頁中使用Consumer引入和修改狀態(tài)
????引入位置一:在body中使用Consumer京景,Consumer需要傳入一個builder回調(diào)函數(shù)窿冯,當數(shù)據(jù)發(fā)生變化時,就會通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來構(gòu)建确徙;
????引入位置二:在floatingActionButton中使用Consumer醒串,當點擊按鈕時执桌,修改CounterNotifier中的counter數(shù)據(jù);
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("當前計數(shù):${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方法解析:
????參數(shù)一:context芜赌,每個build方法都會有上下文仰挣,目的是知道當前樹的位置
????參數(shù)二:ChangeNotifier對應(yīng)的實例,也是我們在builder函數(shù)中主要使用的對象
????參數(shù)三:child缠沈,目的是進行優(yōu)化膘壶,如果builder下面有一顆龐大的子樹,當模型發(fā)生改變的時候洲愤,我們并不希望重新build這顆子樹颓芭,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)
步驟四:創(chuàng)建一個新的頁面柬赐,在新的頁面中修改數(shù)據(jù)
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.2.2. Provider.of的弊端
事實上亡问,因為Provider是基于InheritedWidget,所以我們在使用ChangeNotifier中的數(shù)據(jù)時肛宋,我們可以通過Provider.of的方式來使用州藕,比如下面的代碼:
Text("當前計數(shù):${Provider.of<CounterProvider>(context).counter}",
? style: TextStyle(fontSize: 30, color: Colors.purple),
),
我們會發(fā)現(xiàn)很明顯上面的代碼會更加簡潔,那么開發(fā)中是否要選擇上面這種方式呢酝陈?
????答案是否定的床玻,更多時候我們還是要選擇Consumer的方式。
為什么呢沉帮?因為Consumer在刷新整個Widget樹時笨枯,會盡可能少的rebuild Widget。
方式一:Provider.of的方式完整的代碼:
????當我們點擊了floatingActionButton時遇西,HYHomePage的build方法會被重新調(diào)用馅精。
????這意味著整個HYHomePage的Widget都需要重新build
class HYHomePage extends StatelessWidget {
? @override
? Widget build(BuildContext context) {
? ? print("調(diào)用了HYHomePage的build方法");
? ? return Scaffold(
? ? ? appBar: AppBar(
? ? ? ? title: Text("Provider"),
? ? ? ),
? ? ? body: Center(
? ? ? ? child: Column(
? ? ? ? ? mainAxisAlignment: MainAxisAlignment.center,
? ? ? ? ? children: <Widget>[
? ? ? ? ? ? Text("當前計數(shù):${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中的內(nèi)容采用Consumer的方式修改如下:
????你會發(fā)現(xiàn)HYHomePage的build方法不會被重新調(diào)用;
????設(shè)置如果我們有對應(yīng)的child widget粱檀,可以采用上面案例中的方式來組織洲敢,性能更高;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
? print("調(diào)用Consumer的builder");
? return Text(
? ? "當前計數(shù):${counterPro.counter}",
? ? style: TextStyle(fontSize: 30, color: Colors.red),
? );
}),
2.2.3. Selector的選擇
Consumer是否是最好的選擇呢茄蚯?并不是压彭,它也會存在弊端
????比如當點擊了floatingActionButton時,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用渗常;
????我們會發(fā)現(xiàn)只要點擊了floatingActionButton壮不,兩個位置都會被重新builder;
????但是floatingActionButton的位置有重新build的必要嗎皱碘?沒有询一,因為它是否在操作數(shù)據(jù),并沒有展示;
????如何可以做到讓它不要重新build了健蕊?使用Selector來代替Consumer
我們先直接實現(xiàn)代碼菱阵,在解釋其中的含義:
floatingActionButton: Selector<CounterProvider, CounterProvider>(
? selector: (ctx, provider) => provider,
? shouldRebuild: (pre, next) => false,
? builder: (ctx, counterPro, child) {
? ? print("floatingActionButton展示的位置builder被調(diào)用");
? ? return FloatingActionButton(
? ? ? child: child,
? ? ? onPressed: () {
? ? ? ? counterPro.counter += 1;
? ? ? },
? ? );
? },
? child: Icon(Icons.add),
),
Selector和Consumer對比,不同之處主要是三個關(guān)鍵點:
????關(guān)鍵點1:泛型參數(shù)是兩個
????????泛型參數(shù)一:我們這次要使用的Provider
????????泛型參數(shù)二:轉(zhuǎn)換之后的數(shù)據(jù)類型缩功,比如我這里轉(zhuǎn)換之后依然是使用CounterProvider晴及,那么他們兩個就是一樣的類型
????關(guān)鍵點2:selector回調(diào)函數(shù)
????????轉(zhuǎn)換的回調(diào)函數(shù),你希望如何進行轉(zhuǎn)換
????????S Function(BuildContext, A) selector
? ? ? ? 我這里沒有進行轉(zhuǎn)換嫡锌,所以直接將A實例返回即可
????關(guān)鍵點3:是否希望重新rebuild
????????這里也是一個回調(diào)函數(shù)虑稼,我們可以拿到轉(zhuǎn)換前后的兩個實例;
????????bool Function(T previous, T next);
????????因為這里我不希望它重新rebuild势木,無論數(shù)據(jù)如何變化蛛倦,所以這里我直接return false;
這個時候跟压,我們重新測試點擊floatingActionButton胰蝠,floatingActionButton中的代碼并不會進行rebuild操作歼培。
所以在某些情況下震蒋,我們可以使用Selector來代替Consumer,性能會更高躲庄。
2.2.4. MultiProvider
在開發(fā)中查剖,我們需要共享的數(shù)據(jù)肯定不止一個,并且數(shù)據(jù)之間我們需要組織到一起噪窘,所以一個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;
? }
}
如果在開發(fā)中我們有多個Provider需要提供應(yīng)該怎么做呢?
方式一:多個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(),
));