一. 為什么需要狀態(tài)管理?
1.1. 認(rèn)識(shí)狀態(tài)管理
很多從命令式編程框架(Android或iOS原生開發(fā)者)轉(zhuǎn)成聲明式編程(Flutter、Vue、React等)剛開始并不適應(yīng),因?yàn)樾枰粋€(gè)新的角度來考慮APP的開發(fā)模式。
Flutter作為一個(gè)現(xiàn)代的框架,是聲明式編程的:
在編寫一個(gè)應(yīng)用的過程中,我們有大量的State需要來進(jìn)行管理,而正是對這些State的改變,來更新界面的刷新:
1.2. 不同狀態(tài)管理分類
1.2.1. 短時(shí)狀態(tài)Ephemeral state
某些狀態(tài)只需要在自己的Widget中使用即可
- 比如我們之前做的簡單計(jì)數(shù)器counter
- 比如一個(gè)PageView組件記錄當(dāng)前的頁面
- 比如一個(gè)動(dòng)畫記錄當(dāng)前的進(jìn)度
- 比如一個(gè)BottomNavigationBar中當(dāng)前被選中的tab
這種狀態(tài)我們只需要使用StatefulWidget對應(yīng)的State類自己管理即可,Widget樹中的其它部分并不需要訪問這個(gè)狀態(tài)。
1.2.2. 應(yīng)用狀態(tài)App state
開發(fā)中也有非常多的狀態(tài)需要在多個(gè)部分進(jìn)行共享
- 比如用戶一個(gè)個(gè)性化選項(xiàng)
- 比如用戶的登錄狀態(tài)信息
- 比如一個(gè)電商應(yīng)用的購物車
- 比如一個(gè)新聞應(yīng)用的已讀消息或者未讀消息
這種狀態(tài)我們?nèi)绻赪idget之間傳遞來、傳遞去,那么是無窮盡的,并且代碼的耦合度會(huì)變得非常高,牽一發(fā)而動(dòng)全身,無論是代碼編寫質(zhì)量、后期維護(hù)、可擴(kuò)展性都非常差。
這個(gè)時(shí)候我們可以選擇全局狀態(tài)管理的方式,來對狀態(tài)進(jìn)行統(tǒng)一的管理和應(yīng)用。
1.2.3. 如何選擇不同的管理方式
開發(fā)中,沒有明確的規(guī)則去區(qū)分哪些狀態(tài)是短時(shí)狀態(tài),哪些狀態(tài)是應(yīng)用狀態(tài)。
- 某些短時(shí)狀態(tài)可能在之后的開發(fā)維護(hù)中需要升級(jí)為應(yīng)用狀態(tài)。
但是我們可以簡單遵守下面這幅流程圖的規(guī)則:
經(jīng)驗(yàn)原則就是:選擇能夠減少麻煩的方式。
二. 共享狀態(tài)管理
2.1. InheritedWidget
InheritedWidget和React中的context功能類似,可以實(shí)現(xiàn)跨組件數(shù)據(jù)的傳遞。
定義一個(gè)共享數(shù)據(jù)的InheritedWidget,需要繼承自InheritedWidget
- 這里定義了一個(gè)of方法,該方法通過context開始去查找祖先的HYDataWidget(可以查看源碼查找過程)
- updateShouldNotify方法是對比新舊HYDataWidget,是否需要對更新相關(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)聽
2.2. Provider
Provider是目前官方推薦的全局狀態(tài)管理工具,使用之前,我們需要先引入對它的依賴,
dependencies:
provider: ^4.0.4
2.2.1. Provider的基本使用
在使用Provider的時(shí)候,我們主要關(guān)心三個(gè)概念:
- ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
- ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方,會(huì)在其中創(chuàng)建對應(yīng)的ChangeNotifier
- Consumer:Widget樹中需要使用數(shù)據(jù)(狀態(tài))的地方
我們先來完成一個(gè)簡單的案例,將官方計(jì)數(shù)器案例使用Provider來實(shí)現(xiàn):
第一步:創(chuàng)建自己的ChangeNotifier
我們需要一個(gè)ChangeNotifier來保存我們的狀態(tài),所以創(chuàng)建它
- 這里我們可以使用繼承自ChangeNotifier,也可以使用混入,這取決于概率是否需要繼承自其它的類
- 我們使用一個(gè)私有的_counter,并且提供了getter和setter
- 在setter中我們監(jiān)聽到_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(),
));
}
第三步:在首頁中使用Consumer引入和修改狀態(tài)
- 引入位置一:在body中使用Consumer,Consumer需要傳入一個(gè)builder回調(diào)函數(shù),當(dāng)數(shù)據(jù)發(fā)生變化時(shí),就會(huì)通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來構(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("列表測試"),
),
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ù)二:ChangeNotifier對應(yīng)的實(shí)例,也是我們在builder函數(shù)中主要使用的對象
- 參數(shù)三:child,目的是進(jìn)行優(yōu)化,如果builder下面有一顆龐大的子樹,當(dāng)模型發(fā)生改變的時(shí)候,我們并不希望重新build這顆子樹,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)
步驟四:創(chuàng)建一個(gè)新的頁面,在新的頁面中修改數(shù)據(jù)
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二個(gè)頁面"),
),
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,所以我們在使用ChangeNotifier中的數(shù)據(jù)時(shí),我們可以通過Provider.of的方式來使用,比如下面的代碼:
Text("當(dāng)前計(jì)數(shù):${Provider.of<CounterProvider>(context).counter}",
style: TextStyle(fontSize: 30, color: Colors.purple),
),
我們會(huì)發(fā)現(xiàn)很明顯上面的代碼會(huì)更加簡潔,那么開發(fā)中是否要選擇上面這種方式了?
- 答案是否定的,更多時(shí)候我們還是要選擇Consumer的方式。
為什么呢?因?yàn)镃onsumer在刷新整個(gè)Widget樹時(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è)置如果我們有對應(yīng)的child widget,可以采用上面案例中的方式來組織,性能更高;
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í),我們在代碼的兩處分別打印它們的builder是否會(huì)重新調(diào)用;
- 我們會(huì)發(fā)現(xiàn)只要點(diǎn)擊了floatingActionButton,兩個(gè)位置都會(huì)被重新builder;
- 但是floatingActionButton的位置有重新build的必要嗎?沒有,因?yàn)樗欠裨诓僮鲾?shù)據(jù),并沒有展示;
- 如何可以做到讓它不要重新build了?使用Selector來代替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對比,不同之處主要是三個(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
- 我這里沒有進(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,無論數(shù)據(jù)如何變化,所以這里我直接return false;
這個(gè)時(shí)候,我們重新測試點(diǎn)擊floatingActionButton,floatingActionButton中的代碼并不會(huì)進(jìn)行rebuild操作。
所以在某些情況下,我們可以使用Selector來代替Consumer,性能會(huì)更高。
2.2.4. MultiProvider
在開發(fā)中,我們需要共享的數(shù)據(jù)肯定不止一個(gè),并且數(shù)據(jù)之間我們需要組織到一起,所以一個(gè)Provider必然是不夠的。
我們在增加一個(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;
}
}
如果在開發(fā)中我們有多個(gè)Provider需要提供應(yīng)該怎么做呢?
方式一:多個(gè)Provider之間嵌套
- 這樣做有很大的弊端,如果嵌套層級(jí)過多不方便維護(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(),
));