狀態(tài)管理在Flutter中非常重要,但是它包含的內(nèi)容又非常的廣泛。
本文我們首先了解下什么是狀態(tài)和狀態(tài)管理呢?然后我們來了解官方的狀態(tài)管理庫Provider的使用,最后分析下Provider背后的秘密。
狀態(tài)管理
狀態(tài)
Flutter是聲明式編程,Widget定義的UI都是在build()
函數(shù)中實(shí)現(xiàn)的,這個(gè)函數(shù)的功能就是將狀態(tài)轉(zhuǎn)換成UI。
UI = f(state)
官方對(duì)狀態(tài)的定義如下:
whatever data you need in order to rebuild your UI at any moment in time
翻譯過來就是:狀態(tài)就是任何時(shí)間任何場(chǎng)景下重構(gòu)UI所需要的數(shù)據(jù)。
這里面至少可以看到兩層含義:
- 狀態(tài)就是數(shù)據(jù);
- 狀態(tài)的改變驅(qū)動(dòng)了UI的改變。
狀態(tài)的分類
我們可以把狀態(tài)分為局部狀態(tài)和全局狀態(tài)。
局部狀態(tài)就是Widget中內(nèi)部持有的狀態(tài),典型代表就是StatefuleWidget和它對(duì)應(yīng)的State。局部狀態(tài)只會(huì)影響單個(gè)Widget的UI呈現(xiàn)。
當(dāng)某個(gè)狀態(tài)需要在多個(gè)Widget使用,或者在整個(gè)APP中使用,那它就是全局狀態(tài)了。全局狀態(tài)的典型代表就是InheritedWidget。
我們?cè)?a target="_blank">InheritedWidget的使用和源碼分析這篇文章中已經(jīng)詳細(xì)介紹過了InheritedWidget的相關(guān)內(nèi)容,當(dāng)然我們也提到過它的一些不是太完善的地方。
狀態(tài)管理庫
我們這里所說的狀態(tài)管理庫主要是指對(duì)全局狀態(tài)的一些處理庫,除了InheritedWidget外,還有一些最近非常流行的庫:
它目前是評(píng)分最高的庫,適合大型的項(xiàng)目。但是它有一個(gè)缺點(diǎn)就是理解起來比較困難,編寫代碼方式也很獨(dú)特,需要編寫一些重復(fù)的代碼模板。
它是Flutter官方團(tuán)隊(duì)共同維護(hù)的一個(gè)項(xiàng)目,由于有官方背景,所以不用擔(dān)心后期的維護(hù)升級(jí)問題。
getx是目前上升趨勢(shì)最快的一個(gè)庫,使用非常簡(jiǎn)單,代碼也很簡(jiǎn)介,功能很多。
當(dāng)然還有其他一些庫,譬如mobx,flutter_redux等,當(dāng)然你很大可能也不會(huì)用到。
我們將會(huì)對(duì)Provider和getx這兩個(gè)庫的使用和源碼進(jìn)行介紹。
Provider的使用
和介紹InheritedWidget時(shí)使用的案例類似,本文講解Provider的時(shí)候使用是一個(gè)簡(jiǎn)單的計(jì)數(shù)器案例:有一個(gè)number的全局狀態(tài),有三個(gè)Widget會(huì)使用到它,點(diǎn)擊FloatingActionButton可以將number的值加1。效果如下:
當(dāng)然復(fù)雜的多界面邏輯的實(shí)現(xiàn)方法使用的方法是一樣的。譬如實(shí)現(xiàn)下面的功能:
基本使用
使用前得先引入庫:
dependencies:
provider: ^5.0.0
接下來我們分三步來了解它的使用:
- 將number封裝到ChangeNotifier中,創(chuàng)建需要共享的狀態(tài)
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
ChangeNotifier是Flutter Framework的基礎(chǔ)類,不是Provider庫中的類。ChangeNotifier繼承自Listenable,也就是ChangeNotifier可以通知觀察者值的改變(實(shí)現(xiàn)了觀察者模式)。
NumberModel有一個(gè)_number
狀態(tài),然后提供了獲取的方法get
和設(shè)置set
的方法。
- 在應(yīng)用程序的頂層添加ChangeNotifierProvider
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
child: MyApp(),
),
);
}
將應(yīng)用的頂層設(shè)置為ChangeNotifierProvider, 然后將MyApp()變?yōu)樗?strong>子Widget。
ChangeNotifierProvider的create
函數(shù)需要返回ChangeNotifier。
- 其它Widget使用共享的狀態(tài)
有四個(gè)地方需要使用到共享的狀態(tài),三個(gè)顯示文字的Text Widget和FloatingActionButton。
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 獲取NumberModel的number
int number = Provider.of<NumberModel>(context).number;
return Container(
child: Text(
"點(diǎn)擊次數(shù): $number",
style: TextStyle(fontSize: 30),
),
);
}
}
我們將Text Widget封裝成了NumberWidget1, 通過int number = Provider.of<NumberModel>(context).number;
獲取到NumberModel的number
值,然后就可以顯示了。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1 獲取NumberModel
NumberModel model = Provider.of<NumberModel>(context);
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 2 修改number值
model.number++;
},
child: Icon(Icons.add),
));
}
}
FloatingActionButton也需要通過Provider.of<NumberModel>(context)
方法先拿到NumberModel,然后調(diào)用set
方法改變number
的值。
全部代碼:
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
NumberModel model = Provider.of<NumberModel>(context);
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
model.number++;
},
child: Icon(Icons.add),
));
}
}
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
int number = Provider.of<NumberModel>(context).number;
return Container(
child: Text(
"點(diǎn)擊次數(shù): $number",
style: TextStyle(fontSize: 30),
),
);
}
}
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
Consumer
問題:
Provider.of
有一個(gè)問題,就是當(dāng)狀態(tài)值發(fā)生變化后,Provider.of
所在的Widget整個(gè)buil
d方法都會(huì)重新構(gòu)建。
上面的例子中,FloatingActionButton會(huì)引起Scaffold的重構(gòu),所以對(duì)性能的影響是最大的。
Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: Icon(Icons.add),
);
},
)
我們將FloatingActionButton用Consumer包裹,builder
中的value
參數(shù)就是我們需要的NumberModel了。
這里我們可以進(jìn)一步優(yōu)化一下,對(duì)child進(jìn)行復(fù)用。
Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
我們將child傳入Consumer的構(gòu)造函數(shù)就能實(shí)現(xiàn)復(fù)用了。
child復(fù)用的邏輯我們?cè)谇耙黄P(guān)于動(dòng)畫源碼的文章中有解釋,如果需要可以回頭參閱。
差異部分的代碼如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
}
}
Consumer
問題:
Consumer
總歸還是需要重構(gòu)的,其實(shí)我們使用FloatingActionButton的時(shí)候只是用到了NumberModel的設(shè)置方法,根本沒有用到它的_number
屬性,所以即使_number
改變了,我們也是可以不需要重構(gòu)的。
如果不需要重構(gòu),我們可以使用Selector:
Selector<NumberModel, NumberModel>(
selector: (ctx, numberModel) => numberModel,
shouldRebuild: (previous, next) => false,
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
)
代碼解釋:
- Selector的泛型中有兩個(gè)參數(shù)類型,第一個(gè)是原始類型,第二個(gè)是轉(zhuǎn)換后的類型,也就是說Selector多了一個(gè)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換的功能;
-
selector
是進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換的函數(shù); -
shouldRebuild
是確實(shí)是否需要重構(gòu),我們明顯是不需要的,所以傳false; -
builder
和Consumer的功能就是類似的了。
差異部分的代碼如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: Selector<NumberModel, NumberModel>(
selector: (ctx, numberModel) => numberModel,
shouldRebuild: (previous, next) => false,
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
}
}
多個(gè)狀態(tài)的使用
有時(shí)候某個(gè)Widget可能需要使用多個(gè)狀態(tài),我們接下來就介紹這種情況的使用方法。
- 創(chuàng)建多個(gè)需要共享的狀態(tài)
class RandomNumberModel extends ChangeNotifier {
int _randomNumber = Random().nextInt(100);
int get randomNumber => _randomNumber;
void resetRandomNumber() {
_randomNumber = Random().nextInt(100);
notifyListeners();
}
}
我們?cè)賱?chuàng)建一個(gè)RandomNumberModel,里面有一個(gè)隨機(jī)的數(shù)值_randomNumber
, 并且設(shè)置獲取方法get
和設(shè)置方法resetRandomNumber
。
- 將應(yīng)用程序的頂層改為MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<NumberModel>(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider<RandomNumberModel>(
create: (ctx) => RandomNumberModel(),
),
],
child: MyApp(),
),
);
}
MultiProvider的providers
放置的是共享的多個(gè)Provider。
- 其它Widget使用共享的狀態(tài)
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 讀取
int number = Provider.of<NumberModel>(context).number;
// 讀取
int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
return Container(
// 使用
child: Text(
"點(diǎn)擊次數(shù): $number 隨機(jī)數(shù): $randomNumber",
style: TextStyle(fontSize: 30),
),
);
}
}
我們可以通過Provider.of
分別取到NumberModel和RandomNumberModel,然后讀取到相應(yīng)的值。
Consumer2
class NumberWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer2<NumberModel, RandomNumberModel>(
builder: (context, value, value2, child) {
return Text("點(diǎn)擊次數(shù): ${value.number} 隨機(jī)數(shù): ${value2.randomNumber}",
style: TextStyle(fontSize: 30));
},
),
);
}
}
Consumer2中兩個(gè)泛型代表使用的哪兩個(gè)數(shù)據(jù),build
方法中的value
就是NumberModel,value2
就是RandomNumberModel,然后讀取到相應(yīng)的值。
Selector2
class NumberWidget3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
builder: (context, value, child) {
return Text("點(diǎn)擊次數(shù): ${value.item1} 隨機(jī)數(shù): ${value.item2}",
style: TextStyle(fontSize: 30));
},
shouldRebuild: (previous, next) => previous != next,
)
);
}
}
-
Selector2有三個(gè)泛型參數(shù):NumberModel和RandomNumberModel代表使用的兩個(gè)數(shù)據(jù)類型,第三個(gè)參數(shù)表示由前兩個(gè)數(shù)據(jù)轉(zhuǎn)換成的新的數(shù)據(jù)類型,我們需要使用兩個(gè)
int
值。
使用Tuple2需要引入三方庫
tuple: ^2.0.0
。使用它的優(yōu)點(diǎn)是它內(nèi)置了==
比較操作符,不需要我們?nèi)プ约罕容^元素是否相等了。
-
selector
的三個(gè)參數(shù)為:BuildContext,NumberModel和RandomNumberModel, 返回值就是轉(zhuǎn)換后的數(shù)據(jù)。
builder
方法中就可以直接使用value.item1
和value.item2
了。
-
shouldRebuild
方法的previous
和next
的類型是Tuple2<int, int>,可以直接比較。如果相同就不重構(gòu)了。
多個(gè)狀態(tài)使用的補(bǔ)充
Consumer2還有幾個(gè)好兄弟:,Consumer3,Consumer4,Consumer5,Consumer6。
Selector2也有幾個(gè)好兄弟:,Selector3,Selector4,Selector5,Selector6。
通過名字可以知道,他們分別可以組合對(duì)應(yīng)的多個(gè)數(shù)據(jù)。
全部代碼:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<NumberModel>(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider<RandomNumberModel>(
create: (ctx) => RandomNumberModel(),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
),
floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
child: Icon(Icons.add),
builder: (context, value, value2, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
value2.resetRandomNumber();
},
child: child,
);
},
),
);
}
}
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
int number = Provider.of<NumberModel>(context).number;
int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
return Container(
child: Text(
"點(diǎn)擊次數(shù): $number 隨機(jī)數(shù): $randomNumber",
style: TextStyle(fontSize: 30),
),
);
}
}
class NumberWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer2<NumberModel, RandomNumberModel>(
builder: (context, value, value2, child) {
return Text("點(diǎn)擊次數(shù): ${value.number} 隨機(jī)數(shù): ${value2.randomNumber}",
style: TextStyle(fontSize: 30));
},
),
);
}
}
class NumberWidget3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
builder: (context, value, child) {
return Text("點(diǎn)擊次數(shù): ${value.item1} 隨機(jī)數(shù): ${value.item2}",
style: TextStyle(fontSize: 30));
},
shouldRebuild: (previous, next) => previous != next,
)
);
}
}
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
class RandomNumberModel extends ChangeNotifier {
int _randomNumber = Random().nextInt(100);
int get randomNumber => _randomNumber;
void resetRandomNumber() {
_randomNumber = Random().nextInt(100);
notifyListeners();
}
}
Provider源碼解析
- Provider的基本架構(gòu)如下:
- 所有的Provider都繼承自InheritedProvider;
-
InheritedProvider持有一個(gè)_CreateInheritedProvider對(duì)象
_delegate
,_delegate
持有_ValueInheritedProviderState對(duì)象,_ValueInheritedProviderState對(duì)象通過createState()
方法調(diào)用了InheritedProvider的create()
方法生成了_value
,_value
也就是開發(fā)者提供的可監(jiān)測(cè)對(duì)象ChangeNotifier;
create()
只有在需要使用_value
時(shí)候才會(huì)調(diào)用,并不是InheritedProvider插入Widget Tree時(shí)候就調(diào)用,屬于懶加載的實(shí)現(xiàn)。
-
InheritedProvider有一個(gè)InheritedWidget的子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的
_value
的值;
也就是說Provider依賴于InheritedWidget,找到對(duì)應(yīng)的InheritedWidget就能獲取對(duì)應(yīng)的
_value
的值。
-
Widget重構(gòu)的時(shí)候如果調(diào)用
Provider.of
方法,會(huì)找到_value
的值并且監(jiān)聽它的變化。
- Provider的局部刷新邏輯如下:
-
_value
值發(fā)生變化,會(huì)通知監(jiān)聽者刷新。其中會(huì)調(diào)用_InheritedProviderScope的markNeedsNotifyDependents
方法,調(diào)用依賴Widget的didChangeDependencies
, 這兩個(gè)方法都會(huì)調(diào)用markNeedsBuild()
,進(jìn)行重構(gòu); -
Widget重構(gòu)的時(shí)候會(huì)調(diào)用
Provider.of
方法,更新對(duì)_value
的監(jiān)聽,為下次重構(gòu)做準(zhǔn)備。
- Consumer和Selector的優(yōu)化邏輯:
Consumer和Selector只是封裝了一層SingleChildStatefulWidget,重構(gòu)的范圍限定在Consumer和Selector內(nèi)部,內(nèi)部調(diào)用的還是Provider.of
方法。
- MultiProvider的邏輯:
MultiProvider就是嵌套了多個(gè)Provider,其他和單個(gè)Provider沒有什么差別。
總結(jié)
其實(shí)Provider庫還提供了其他的幾個(gè)Provider,ListenableProvider
,ValueListenableProvider
,StreamProvider
和FutureProvider
,它們都是我們開發(fā)中的可選項(xiàng)。
至此,我們將Provider庫的使用方式和底層的邏輯解釋完了。