狀態(tài)管理是聲明式編程非常重要的一個(gè)概念贮配,我們?cè)谇懊娼榻B過(guò)Flutter是聲明式編程的,也區(qū)分聲明式編程和命令式編程的區(qū)別塞赂。
這里泪勒,我們就來(lái)系統(tǒng)的學(xué)習(xí)一下Flutter聲明式編程中非常重要的狀態(tài)管理
一. 為什么需要狀態(tài)管理?
1.1. 認(rèn)識(shí)狀態(tài)管理
很多從命令式編程框架(Android或iOS原生開(kāi)發(fā)者)轉(zhuǎn)成聲明式編程(Flutter宴猾、Vue圆存、React等)剛開(kāi)始并不適應(yīng),因?yàn)樾枰粋€(gè)新的角度來(lái)考慮APP的開(kāi)發(fā)模式仇哆。
Flutter作為一個(gè)現(xiàn)代的框架沦辙,是聲明式編程的:
在編寫(xiě)一個(gè)應(yīng)用的過(guò)程中,我們有大量的State需要來(lái)進(jìn)行管理讹剔,而正是對(duì)這些State的改變油讯,來(lái)更新界面的刷新:
1.2. 不同狀態(tài)管理分類
1.2.1. 短時(shí)狀態(tài)Ephemeral state
某些狀態(tài)只需要在自己的Widget中使用即可
- 比如我們之前做的簡(jiǎn)單計(jì)數(shù)器counter
- 比如一個(gè)PageView組件記錄當(dāng)前的頁(yè)面
- 比如一個(gè)動(dòng)畫(huà)記錄當(dāng)前的進(jìn)度
- 比如一個(gè)BottomNavigationBar中當(dāng)前被選中的tab
這種狀態(tài)我們只需要使用StatefulWidget對(duì)應(yīng)的State類自己管理即可详民,Widget樹(shù)中的其它部分并不需要訪問(wèn)這個(gè)狀態(tài)。
這種方式在之前的學(xué)習(xí)中撞羽,我們已經(jīng)應(yīng)用過(guò)非常多次了阐斜。
1.2.2. 應(yīng)用狀態(tài)App state
開(kāi)發(fā)中也有非常多的狀態(tài)需要在多個(gè)部分進(jìn)行共享
- 比如用戶一個(gè)個(gè)性化選項(xiàng)
- 比如用戶的登錄狀態(tài)信息
- 比如一個(gè)電商應(yīng)用的購(gòu)物車
- 比如一個(gè)新聞應(yīng)用的已讀消息或者未讀消息
這種狀態(tài)我們?nèi)绻赪idget之間傳遞來(lái)、傳遞去诀紊,那么是無(wú)窮盡的谒出,并且代碼的耦合度會(huì)變得非常高,牽一發(fā)而動(dòng)全身邻奠,無(wú)論是代碼編寫(xiě)質(zhì)量笤喳、后期維護(hù)、可擴(kuò)展性都非常差碌宴。
這個(gè)時(shí)候我們可以選擇全局狀態(tài)管理的方式杀狡,來(lái)對(duì)狀態(tài)進(jìn)行統(tǒng)一的管理和應(yīng)用。
1.2.3. 如何選擇不同的管理方式
開(kāi)發(fā)中贰镣,沒(méi)有明確的規(guī)則去區(qū)分哪些狀態(tài)是短時(shí)狀態(tài)呜象,哪些狀態(tài)是應(yīng)用狀態(tài)。
- 某些短時(shí)狀態(tài)可能在之后的開(kāi)發(fā)維護(hù)中需要升級(jí)為應(yīng)用狀態(tài)碑隆。
但是我們可以簡(jiǎn)單遵守下面這幅流程圖的規(guī)則:
針對(duì)React使用setState還是Redux中的Store來(lái)管理狀態(tài)哪個(gè)更好的問(wèn)題恭陡,Redux的issue上,Redux的作者Dan Abramov上煤,它這樣回答的:
The rule of thumb is: Do whatever is less awkward
經(jīng)驗(yàn)原則就是:選擇能夠減少麻煩的方式休玩。
二. 共享狀態(tài)管理
2.1. InheritedWidget
InheritedWidget和React中的context功能類似,可以實(shí)現(xiàn)跨組件數(shù)據(jù)的傳遞劫狠。
定義一個(gè)共享數(shù)據(jù)的InheritedWidget拴疤,需要繼承自InheritedWidget
- 這里定義了一個(gè)of方法,該方法通過(guò)context開(kāi)始去查找祖先的HYDataWidget(可以查看源碼查找過(guò)程)
- updateShouldNotify方法是對(duì)比新舊HYDataWidget独泞,是否需要對(duì)更新相關(guān)依賴的Widget
class HYDataWidget extends InheritedWidget {
final int counter;
HYDataWidget({this.counter, Widget child}): super(child: child);
static HYDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}
@override
bool updateShouldNotify(HYDataWidget oldWidget) {
return this.counter != oldWidget.counter;
}
}
創(chuàng)建HYDataWidget呐矾,并且傳入數(shù)據(jù)(這里點(diǎn)擊按鈕會(huì)修改數(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++;
});
},
),
);
}
}
在某個(gè)Widget中使用共享的數(shù)據(jù)懦砂,并且監(jiān)聽(tīng)
2.2. Provider
Provider是目前官方推薦的全局狀態(tài)管理工具凫佛,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫(xiě)。
使用之前孕惜,我們需要先引入對(duì)它的依賴,截止這篇文章晨炕,Provider的最新版本為4.0.4:
dependencies:
provider: ^4.0.4
2.2.1. Provider的基本使用
在使用Provider的時(shí)候衫画,我們主要關(guān)心三個(gè)概念:
- ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
- ChangeNotifierProvider:Widget樹(shù)中提供數(shù)據(jù)(狀態(tài))的地方,會(huì)在其中創(chuàng)建對(duì)應(yīng)的ChangeNotifier
- Consumer:Widget樹(shù)中需要使用數(shù)據(jù)(狀態(tài))的地方
我們先來(lái)完成一個(gè)簡(jiǎn)單的案例瓮栗,將官方計(jì)數(shù)器案例使用Provider來(lái)實(shí)現(xiàn):
第一步:創(chuàng)建自己的ChangeNotifier
我們需要一個(gè)ChangeNotifier來(lái)保存我們的狀態(tài)削罩,所以創(chuàng)建它
- 這里我們可以使用繼承自ChangeNotifier瞄勾,也可以使用混入,這取決于概率是否需要繼承自其它的類
- 我們使用一個(gè)私有的_counter弥激,并且提供了getter和setter
- 在setter中我們監(jiān)聽(tīng)到_counter的改變进陡,就調(diào)用notifyListeners方法,通知所有的Consumer進(jìn)行更新
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可以獲取到數(shù)據(jù):
- 將ChangeNotifierProvider放到了頂層趾疚,這樣方便在整個(gè)應(yīng)用的任何地方可以使用CounterProvider
void main() {
runApp(ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
));
}
第三步:在首頁(yè)中使用Consumer引入和修改狀態(tài)
- 引入位置一:在body中使用Consumer,Consumer需要傳入一個(gè)builder回調(diào)函數(shù)以蕴,當(dāng)數(shù)據(jù)發(fā)生變化時(shí)糙麦,就會(huì)通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來(lái)構(gòu)建;
- 引入位置二:在floatingActionButton中使用Consumer丛肮,當(dāng)點(diǎn)擊按鈕時(shí)赡磅,修改CounterNotifier中的counter數(shù)據(jù);
class HYHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("列表測(cè)試"),
),
body: Center(
child: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return Text("當(dāng)前計(jì)數(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宝与,每個(gè)build方法都會(huì)有上下文焚廊,目的是知道當(dāng)前樹(shù)的位置
- 參數(shù)二:ChangeNotifier對(duì)應(yīng)的實(shí)例,也是我們?cè)赽uilder函數(shù)中主要使用的對(duì)象
- 參數(shù)三:child习劫,目的是進(jìn)行優(yōu)化咆瘟,如果builder下面有一顆龐大的子樹(shù),當(dāng)模型發(fā)生改變的時(shí)候榜聂,我們并不希望重新build這顆子樹(shù)搞疗,那么就可以將這顆子樹(shù)放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)
步驟四:創(chuàng)建一個(gè)新的頁(yè)面须肆,在新的頁(yè)面中修改數(shù)據(jù)
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二個(gè)頁(yè)面"),
),
floatingActionButton: Consumer<CounterProvider>(
builder: (ctx, counterPro, child) {
return FloatingActionButton(
child: child,
onPressed: () {
counterPro.counter += 1;
},
);
},
child: Icon(Icons.add),
),
);
}
}
2.2.2. Provider.of的弊端
事實(shí)上匿乃,因?yàn)镻rovider是基于InheritedWidget,所以我們?cè)谑褂肅hangeNotifier中的數(shù)據(jù)時(shí)豌汇,我們可以通過(guò)Provider.of的方式來(lái)使用幢炸,比如下面的代碼:
Text("當(dāng)前計(jì)數(shù):${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
),
我們會(huì)發(fā)現(xiàn)很明顯上面的代碼會(huì)更加簡(jiǎn)潔,那么開(kāi)發(fā)中是否要選擇上面這種方式呢拒贱?
- 答案是否定的宛徊,更多時(shí)候我們還是要選擇Consumer的方式。
為什么呢逻澳?因?yàn)镃onsumer在刷新整個(gè)Widget樹(shù)時(shí)闸天,會(huì)盡可能少的rebuild Widget。
方式一:Provider.of的方式完整的代碼:
- 當(dāng)我們點(diǎn)擊了floatingActionButton時(shí)斜做,HYHomePage的build方法會(huì)被重新調(diào)用苞氮。
- 這意味著整個(gè)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("當(dāng)前計(jì)數(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的方式修改如下:
- 你會(huì)發(fā)現(xiàn)HYHomePage的build方法不會(huì)被重新調(diào)用;
- 設(shè)置如果我們有對(duì)應(yīng)的child widget瓤逼,可以采用上面案例中的方式來(lái)組織笼吟,性能更高库物;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
print("調(diào)用Consumer的builder");
return Text(
"當(dāng)前計(jì)數(shù):${counterPro.counter}",
style: TextStyle(fontSize: 30, color: Colors.red),
);
}),
2.2.3. Selector的選擇
Consumer是否是最好的選擇呢?并不是贷帮,它也會(huì)存在弊端
- 比如當(dāng)點(diǎn)擊了floatingActionButton時(shí)戚揭,我們?cè)诖a的兩處分別打印它們的builder是否會(huì)重新調(diào)用;
- 我們會(huì)發(fā)現(xiàn)只要點(diǎn)擊了floatingActionButton撵枢,兩個(gè)位置都會(huì)被重新builder民晒;
- 但是floatingActionButton的位置有重新build的必要嗎?沒(méi)有诲侮,因?yàn)樗欠裨诓僮鲾?shù)據(jù)镀虐,并沒(méi)有展示;
- 如何可以做到讓它不要重新build了沟绪?使用Selector來(lái)代替Consumer
我們先直接實(shí)現(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對(duì)比,不同之處主要是三個(gè)關(guān)鍵點(diǎn):
關(guān)鍵點(diǎn)1:泛型參數(shù)是兩個(gè)
泛型參數(shù)一:我們這次要使用的Provider
泛型參數(shù)二:轉(zhuǎn)換之后的數(shù)據(jù)類型绽慈,比如我這里轉(zhuǎn)換之后依然是使用CounterProvider恨旱,那么他們兩個(gè)就是一樣的類型
關(guān)鍵點(diǎn)2:selector回調(diào)函數(shù)
轉(zhuǎn)換的回調(diào)函數(shù),你希望如何進(jìn)行轉(zhuǎn)換
S Function(BuildContext, A) selector
我這里沒(méi)有進(jìn)行轉(zhuǎn)換坝疼,所以直接將A實(shí)例返回即可
關(guān)鍵點(diǎn)3:是否希望重新rebuild
這里也是一個(gè)回調(diào)函數(shù)搜贤,我們可以拿到轉(zhuǎn)換前后的兩個(gè)實(shí)例;
bool Function(T previous, T next);
因?yàn)檫@里我不希望它重新rebuild钝凶,無(wú)論數(shù)據(jù)如何變化仪芒,所以這里我直接return false;
這個(gè)時(shí)候耕陷,我們重新測(cè)試點(diǎn)擊floatingActionButton掂名,floatingActionButton中的代碼并不會(huì)進(jìn)行rebuild操作。
所以在某些情況下哟沫,我們可以使用Selector來(lái)代替Consumer饺蔑,性能會(huì)更高。
2.2.4. MultiProvider
在開(kāi)發(fā)中嗜诀,我們需要共享的數(shù)據(jù)肯定不止一個(gè)猾警,并且數(shù)據(jù)之間我們需要組織到一起,所以一個(gè)Provider必然是不夠的隆敢。
我們?cè)谠黾右粋€(gè)新的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;
}
}
如果在開(kāi)發(fā)中我們有多個(gè)Provider需要提供應(yīng)該怎么做呢发皿?
方式一:多個(gè)Provider之間嵌套
- 這樣做有很大的弊端,如果嵌套層級(jí)過(guò)多不方便維護(hù)拂蝎,擴(kuò)展性也比較差
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(),
));