Flutter開發(fā)(14)- 狀態(tài)State管理

????狀態(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(),

));

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浩习,隨后出現(xiàn)的幾起案子静暂,更是在濱河造成了極大的恐慌,老刑警劉巖谱秽,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洽蛀,死亡現(xiàn)場離奇詭異,居然都是意外死亡疟赊,警方通過查閱死者的電腦和手機郊供,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來近哟,“玉大人驮审,你說我怎么就攤上這事。” “怎么了头岔?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵塔拳,是天一觀的道長。 經(jīng)常有香客問我峡竣,道長靠抑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任适掰,我火速辦了婚禮颂碧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘类浪。我一直安慰自己载城,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布费就。 她就那樣靜靜地躺著诉瓦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪力细。 梳的紋絲不亂的頭發(fā)上睬澡,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音眠蚂,去河邊找鬼煞聪。 笑死,一個胖子當著我的面吹牛逝慧,可吹牛的內(nèi)容都是我干的昔脯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼笛臣,長吁一口氣:“原來是場噩夢啊……” “哼云稚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沈堡,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤静陈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后踱蛀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窿给,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年率拒,在試婚紗的時候發(fā)現(xiàn)自己被綠了崩泡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡猬膨,死狀恐怖角撞,靈堂內(nèi)的尸體忽然破棺而出呛伴,到底是詐尸還是另有隱情,我是刑警寧澤谒所,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布热康,位于F島的核電站,受9級特大地震影響劣领,放射性物質(zhì)發(fā)生泄漏姐军。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一尖淘、第九天 我趴在偏房一處隱蔽的房頂上張望奕锌。 院中可真熱鬧,春花似錦村生、人聲如沸惊暴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辽话。三九已至,卻和暖如春卫病,著一層夾襖步出監(jiān)牢的瞬間油啤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工忽肛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留村砂,地道東北人烂斋。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓屹逛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親汛骂。 傳聞我的和親對象是個殘疾皇子罕模,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容