Flutter(十四)狀態(tài)State管理

狀態(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)代的框架沦辙,是聲明式編程的:

Flutter構(gòu)建應(yīng)用過(guò)程

在編寫(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ī)則:

狀態(tài)管理選擇

針對(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(),
));
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末雳窟,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌封救,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捣作,死亡現(xiàn)場(chǎng)離奇詭異誉结,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)券躁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)惩坑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人也拜,你說(shuō)我怎么就攤上這事以舒。” “怎么了慢哈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵蔓钟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我卵贱,道長(zhǎng)滥沫,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任键俱,我火速辦了婚禮兰绣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘编振。我一直安慰自己缀辩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布踪央。 她就那樣靜靜地躺著臀玄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杯瞻。 梳的紋絲不亂的頭發(fā)上镐牺,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音魁莉,去河邊找鬼睬涧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛旗唁,可吹牛的內(nèi)容都是我干的畦浓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼检疫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼讶请!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤夺溢,失蹤者是張志新(化名)和其女友劉穎论巍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體风响,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘉汰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了状勤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鞋怀。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖持搜,靈堂內(nèi)的尸體忽然破棺而出密似,到底是詐尸還是另有隱情,我是刑警寧澤葫盼,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布残腌,位于F島的核電站,受9級(jí)特大地震影響剪返,放射性物質(zhì)發(fā)生泄漏废累。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一脱盲、第九天 我趴在偏房一處隱蔽的房頂上張望邑滨。 院中可真熱鬧,春花似錦钱反、人聲如沸掖看。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)哎壳。三九已至,卻和暖如春尚卫,著一層夾襖步出監(jiān)牢的瞬間归榕,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工吱涉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留刹泄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓怎爵,卻偏偏與公主長(zhǎng)得像特石,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鳖链,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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