Day11 - Flutter - 狀態(tài)State管理

概述

  • 為什么需要狀態(tài)管理?
  • 共享狀態(tài)管理
一. 為什么需要狀態(tài)管理?
  • 1.1、認(rèn)識狀態(tài)管理
    很多從命令式編程框架(Android或iOS原生開發(fā)者)轉(zhuǎn)成聲明式編程(Flutter、Vue、React等)剛開始并不適應(yīng),因為需要一個新的角度來考慮APP的開發(fā)模式。Flutter作為一個現(xiàn)代的框架,是聲明式編程的:



    在編寫一個應(yīng)用的過程中,我們有大量的State需要來進(jìn)行管理,而正是對這些State的改變,來更新界面的刷新:

  • 1.2、不同狀態(tài)管理分類

    • 1.2.1. 短時狀態(tài)Ephemeral state
      某些狀態(tài)只需要在自己的Widget中使用即可

      • 比如我們之前做的簡單計數(shù)器counter
      • 比如一個PageView組件記錄當(dāng)前的頁面
      • 比如一個動畫記錄當(dāng)前的進(jìn)度
      • 比如一個BottomNavigationBar中當(dāng)前被選中的tab

      這種狀態(tài)我們只需要使用StatefulWidget對應(yīng)的State類自己管理即可,Widget樹中的其它部分并不需要訪問這個狀態(tài)。

    • 1.2.2、應(yīng)用狀態(tài)App state
      開發(fā)中也有非常多的狀態(tài)需要在多個部分進(jìn)行共享

      • 比如用戶一個個性化選項
      • 比如用戶的登錄狀態(tài)信息
      • 比如一個電商應(yīng)用的購物車
      • 比如一個新聞應(yīng)用的已讀消息或者未讀消息

      這種狀態(tài)我們?nèi)绻赪idget之間傳遞來、傳遞去,那么是無窮盡的,并且代碼的耦合度會變得非常高,牽一發(fā)而動全身,無論是代碼編寫質(zhì)量、后期維護(hù)、可擴(kuò)展性都非常差。
      這個時候我們可以選擇全局狀態(tài)管理的方式,來對狀態(tài)進(jìn)行統(tǒng)一的管理和應(yīng)用。

    • 1.2.3、如何選擇不同的管理方式
      開發(fā)中,沒有明確的規(guī)則去區(qū)分哪些狀態(tài)是短時狀態(tài),哪些狀態(tài)是應(yīng)用狀態(tài)。
      某些短時狀態(tài)可能在之后的開發(fā)維護(hù)中需要升級為應(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開始去查找祖先的JKDataWidget(可以查看源碼查找過程)

    • updateShouldNotify方法是對比新舊JKDataWidget,是否需要對更新相關(guān)依賴的Widget

      class JKCounterWidget extends InheritedWidget {
          /* 1.共享的數(shù)據(jù) */
          final int counter;
      
          /* 2.自定義的構(gòu)造方法 */
          JKCounterWidget({this.counter, Widget child}): super(child: child);
      
          /* 3.獲取組件最近的當(dāng)前的InheritedWidget */
          static JKCounterWidget of(BuildContext context) {
             /* 沿著Element樹去找到最近的JKJKCounterElement, 從Element里面取出Widget對象 */
             return context.dependOnInheritedWidgetOfExactType();
          }
      
          /* 4.決定要不要回調(diào)State中didChangeDependencies() */
          /* 如果返回True, 執(zhí)行依賴當(dāng)期的InheritedWidget的State中的didChangeDependencies(),反之不會執(zhí)行didChangeDependencies() */
          @override
          bool updateShouldNotify(InheritedWidget oldWidget) {
              return true;
          }
      }
      

      創(chuàng)建JKDataWidget,并且傳入數(shù)據(jù)(這里點擊按鈕會修改數(shù)據(jù),并且重新build)

      import 'package:flutter/material.dart';
      
      void main() => runApp(MyApp());
      
      class MyApp extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
            return MaterialApp(
                // 啟動要顯示的界面
                home: HomePage(),
            );
         }
      }
      
      class HomePage extends StatefulWidget {
          @override
          _HomePageState createState() => _HomePageState();
      }
      
      class _HomePageState extends State<HomePage> {
          int _counter = 100;
      
          @override
          Widget build(BuildContext context) {
              return Scaffold(
                 appBar: AppBar(
                    title: Text("圖標(biāo)組件示例"),
                 ),
                 body: JKCounterWidget(
                    counter: _counter,
                    child: Center(
                      child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                             JKShowData01(),
                             JKShowData02()
                          ],
                      ),
                   ),
                 ),
                 floatingActionButton: FloatingActionButton(
                   child: Icon(Icons.add),
                   onPressed: () {
                       setState(() {
                          _counter++;
                       });
                   },
                 ),
            );
         }
      }
      
      class JKShowData01 extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
              int counter = JKCounterWidget.of(context).counter;
      
              return Card(
                 color: Colors.brown,
                 child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),),
              );
          }
      }
      
      class JKShowData02 extends StatefulWidget {
         @override
         _JKShowData02State createState() => _JKShowData02State();
      }
      
      class _JKShowData02State extends State<JKShowData02> {
      
         @override
         void didChangeDependencies() {
             // TODO: implement didChangeDependencies
             super.didChangeDependencies();
      
             print('執(zhí)行了');
         }
      
         @override
         Widget build(BuildContext context) {
             int counter = JKCounterWidget.of(context).counter;
      
             return Card(
                color: Colors.green,
                child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),),
             );
         }
      }
      

      在某個Widget中使用共享的數(shù)據(jù),并且監(jiān)聽,如上面的JKShowData01JKShowData02

  • 2.2、Provider
    Provider是目前官方推薦的全局狀態(tài)管理工具,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫。
    使用之前,我們需要先引入對它的依賴,截止這篇文章,Provider的最新版本為 4.1.2,最新的我們可以到 https://pub.dev/ 搜所 provider

    dependencies:
        provider:^4.1.2
    
    • 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)建它

        • 這里我們可以使用繼承(extends)ChangeNotifier,也可以使用混入(with),這取決于概率是否需要繼承自其它的類

        • 我們使用一個私有的_counter,并且提供了getter和setter,提示:可以使用快捷鍵來快速生成 getter和setter 方法,鼠標(biāo)放到類名上,cmd + n

        • 在setter中我們監(jiān)聽到_counter的改變,就調(diào)用notifyListeners方法,通知所有的Consumer進(jìn)行更新

          import 'package:flutter/material.dart';
          /* with 代表混入
           *  Dart不支持多繼承,如果我們的類繼承多個類,那么就要使用 with
           *  如果我們的類沒有繼承于其他的類,那么就可以使用 extends 來繼承ChangeNotifier
           */
          class JKCounterViewModel with ChangeNotifier {
              int _counter = 101;
          
              int get counter => _counter;
          
              set counter(int value) {
                   _counter = value;
          
                   /* 通知所有的監(jiān)聽 */
                   notifyListeners();
               }
          }
          
      • 第二步:在Widget Tree中插入ChangeNotifierProvider
        我們需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以獲取到數(shù)據(jù):將ChangeNotifierProvider放到了頂層這樣方便在整個應(yīng)用的任何地方可以使用CounterProvider

        void main() {
           runApp(
              ChangeNotifierProvider(
                 child: MyApp(),
                 create:  (ctx) => JKCounterViewModel(),
              ),
           );
        }
        
      • 第三步:在首頁中使用Consumer引入和修改狀態(tài)

        • 引入位置一:在body中使用Consumer,Consumer需要傳入一個builder回調(diào)函數(shù),當(dāng)數(shù)據(jù)發(fā)生變化時,就會通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來構(gòu)建;

        • 引入位置二:在floatingActionButton中使用Consumer,當(dāng)點擊按鈕時,修改CounterNotifier中的counter數(shù)據(jù);

          class JKHomePage extends StatelessWidget {
              @override
              Widget build(BuildContext context) {
                  return Scaffold(
                     appBar: AppBar(
                        title: Text("列表測試"),
                     ),
                     body: Center(
                       child: Consumer< JKCounterViewModel >(
                           builder: (ctx, viewModel, child) {
                               return Text("當(dāng)前計數(shù):${viewModel.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
                           }
                        ),
                     ),
                     floatingActionButton: Consumer<JKCounterViewModel>(
                        builder: (ctx, counterPro, child) {
                            return FloatingActionButton(
                                child: child,
                                onPressed: () {
                                    counterPro.counter += 1;
                                },
                            );
                        },
                        child: Icon(Icons.add),
                      ),
                   );
              }
          }
          

          提示:child: Icon(Icons.add) 和 child: child 配合使用是為了避免 Icon(Icons.add) 被重創(chuàng)建

        • Consumer的builder方法解析:
          參數(shù)一:context,每個build方法都會有上下文,目的是知道當(dāng)前樹的位置
          參數(shù)二:ChangeNotifier對應(yīng)的實例,也是我們在builder函數(shù)中主要使用的對象
          參數(shù)三:child,目的是進(jìn)行優(yōu)化,如果builder下面有一顆龐大的子樹,當(dāng)模型發(fā)生改變的時候,我們并不希望重新build這顆子樹,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)

        • 步驟四:在我們抽取出來的 Widget 里面 2 種 使用方式

          class JKShowData01 extends StatelessWidget {
             @override
             Widget build(BuildContext context) {
                int counter = Provider.of<JKCounterViewModel>(context).counter;
          
                print('---Data01 的 Build 的方法---');
          
                return Container(
                     color: Colors.brown,
                     child: Text('當(dāng)前計數(shù):$counter', style: TextStyle(fontSize: 30, color: Colors.white),),
                );
             }
          }
          
          class JKShowData02 extends StatelessWidget {
             @override
             Widget build(BuildContext context) {
          
                  print('---Data02 的 Build 的方法---');
          
                  return Container(
                      color: Colors.green,
                      child: Consumer<JKCounterViewModel>(
                          builder: (ctx, counter, child){
                             print("data02 Consumer build方法被執(zhí)行");
                             return Text('當(dāng)前計數(shù):${counter.counter}', style: TextStyle(fontSize: 30, color: Colors.white),);
                          },
                       ),
                  );
             }
          }
          

          提示:

          • 1、JKShowData01里面直接使用 int counter = Provider.of<JKCounterViewModel>(context).counter;,然后:直接使用 counter,缺點是:每次 counter 更新 JKShowData01 的 Widget build(BuildContext context) {} 都會走,影響性能
          • 2、JKShowData02里面使用 Consumer 就比較好了,JKShowData02 的 Widget build(BuildContext context) {} 不會走,Consumer在刷新整個Widget樹時,會盡可能少的rebuild Widget,一定程度上提高了性能。
      • 2.2.2、Selector 的選擇
        Consumer是否是最好的選擇呢?并不是,它也會存在弊端

        • 比如當(dāng)點擊了floatingActionButton時,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用;
        • 我們會發(fā)現(xiàn)只要點擊了floatingActionButton,兩個位置都會被重新builder;
        • 但是floatingActionButton的位置有重新build的必要嗎?沒有,因為它是否在操作數(shù)據(jù),并沒有展示;
        • 如何可以做到讓它不要重新build了?使用Selector來代替Consumer

        我們先直接實現(xiàn)代碼,在解釋其中的含義:

        floatingActionButton: Selector<JKCounterViewModel, JKCounterViewModel>(
           selector: (ctx, counterVM) => counterVM,
           /* 要不要重新構(gòu)建,true 那么loatingActionButton 的 builder 方法會被執(zhí)行,反之false不會被執(zhí)行*/
           shouldRebuild: (previous, next) => false,
           builder: (ctx, counterVM, child) {
               print('floatingActionButton 的 builder 方法被執(zhí)行');
               return FloatingActionButton(
                  child: child,
                  onPressed: () {
                      counterVM.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ù),你希望如何進(jìn)行轉(zhuǎn)換
          • S Function(BuildContext, A) selector
          • 我這里沒有進(jìn)行轉(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中的代碼并不會進(jìn)行rebuild操作。所以在某些情況下,我們可以使用Selector來代替Consumer,性能會更高。

        • 建議:只是依賴數(shù)據(jù)一般使用 Consumer;如果不是依賴數(shù)據(jù),僅僅是修改數(shù)據(jù)那么一般使用 Selector。
    • 2.2.3. MultiProvider(多個數(shù)據(jù)共享)
      在開發(fā)中,我們需要共享的數(shù)據(jù)肯定不止一個,并且數(shù)據(jù)之間我們需要組織到一起,所以一個Provider必然是不夠的。
      我們在增加一個新的ChangeNotifier:JKUserViewModel

      import 'package:flutter/cupertino.dart';
      import 'package:flutterdemo/day10/model/user_model.dart';
      
      class JKUserViewModel with ChangeNotifier {
           UserModel _userModel;
      
           JKUserViewModel(this._userModel);
      
           UserModel get userModel => _userModel;
      
           set userModel(UserModel value) {
                _userModel = value;
      
                /* 通知所有的監(jiān)聽 */
                notifyListeners();
           }
      }
      
      class UserModel {
          /* 名字 */
          String name;
          /* 等級 */
          int level;
          /* 頭像 */
          String imageUrl;
      
          UserModel(this.name, this.level, this.imageUrl);
      }
      

      我們之前的使用

      void main() {
         runApp(
            ChangeNotifierProvider(
               child: MyApp(),
               create:  (ctx) => JKCounterViewModel(),
             ),
          );
      }
      

      上面僅僅是共享JKCounterViewModel,那么我們也想共享JKUserViewModel,那么怎么處理呢?

      • 方式一:多個Provider之間嵌套,這樣做有很大的弊端,如果嵌套層級過多不方便維護(hù),擴(kuò)展性也比較差

        runApp(
           ChangeNotifierProvider(
              create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://')),
              child: ChangeNotifierProvider(
                  child: MyApp(),
                  create:  (ctx) => JKCounterViewModel(),
              ),
           )
        );
        
      • 方式二:使用 MultiProvider (推薦使用)

        runApp(
           MultiProvider(
              child: MyApp(),
                 providers: [
                    ChangeNotifierProvider(create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://'))),
                    ChangeNotifierProvider(create: (ctx) => JKCounterViewModel(),)
                 ],
              )
           );
        }
        

        提示:建議創(chuàng)建一個文件去管理共享的數(shù)據(jù),比如創(chuàng)建一個 initalize_providers.dart 的類

        List<SingleChildWidget> providers = [
            ChangeNotifierProvider(create: (ctx) => JKUserViewModel(UserModel('wc', 28, 'http://')),),
            ChangeNotifierProvider(create: (ctx) => JKCounterViewModel(),)
        ];
        
        • 在使用的時候我們就可以直接如下

          runApp(
             MultiProvider(
                child: MyApp(),
                providers: providers
             )
          );
          
    • 拓展:在一個Consumer里里使用多個 共享的model,如:Consumer2代表有兩個參數(shù)如下

      Consumer2<JKCounterViewModel, JKUserViewModel>(
           builder: (ctx, counter, user, child){
               return Text('當(dāng)前計數(shù):${counter.counter} 名字:${user.userModel.name}', style: TextStyle(fontSize: 30, color: Colors.white),);
           },
      )
      

      提示一:Consumer2 代表兩個參數(shù),那么 builder: (ctx, counter, user, child)里面也是兩個 共享的model
      提示二:Consumer最多到 Consumer6,再多就要 Consumer 嵌套使用了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,892評論 2 372