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

一. 為什么需要狀態(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
Select的弊端

我們先直接實(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(),
));
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容