(Provider)跨組件狀態(tài)共享

在Flutter開發(fā)中,狀態(tài)管理是一個(gè)永恒的話題。
一般的原則:

  • 如果狀態(tài)是組件私有的,應(yīng)該由組件自己管理
  • 如果狀態(tài)要跨組件共享,則該狀態(tài)由各個(gè)組件共同的父元素來管理

對(duì)于組件私有的狀態(tài)管理很好理解,但是對(duì)于跨組件共享的狀態(tài),管理的方式就比較多了,如果用全局事件總線EventBus,它是一個(gè)觀察者模式的實(shí)現(xiàn),通過它就可以實(shí)現(xiàn)跨組件狀態(tài)同步:狀態(tài)持有方(發(fā)布者)負(fù)責(zé)更新、發(fā)布狀態(tài),狀態(tài)使用方(觀察者)監(jiān)聽狀態(tài)改變事件來執(zhí)行一些操作。

下面我們看一個(gè)登陸狀態(tài)同步的簡(jiǎn)單例子:
定義事件:

enum Event{
  login,
  ... //省略其它事件
}

登陸頁(yè)代碼大致如下:

// 登錄狀態(tài)改變后發(fā)布狀態(tài)改變事件
bus.emit(Event.login);

依賴登錄狀態(tài)的頁(yè)面:

void onLoginChanged(e){
  //登錄狀態(tài)變化處理邏輯
}

@override
void initState() {
  //訂閱登錄狀態(tài)改變事件
  bus.on(Event.login,onLogin);
  super.initState();
}

@override
void dispose() {
  //取消訂閱
  bus.off(Event.login,onLogin);
  super.dispose();
}

我們可以發(fā)現(xiàn),通過觀察者模式來實(shí)現(xiàn)跨組件狀態(tài)共享有一些明顯的缺點(diǎn):

  1. 必須顯示定義各種事件,不好管理
  2. 訂閱者必須顯示注冊(cè)狀態(tài)改變回調(diào),也必須在組件銷毀時(shí)手動(dòng)去解綁回調(diào)避免內(nèi)存泄漏。

Flutter中還有更好的跨組件狀態(tài)管理方式,想想前面的InheritedWidget,它的天生特性就是能綁定InheritedWidget與依賴它的子孫組件的依賴關(guān)系,并且當(dāng)InheritedWidget數(shù)據(jù)發(fā)生變化時(shí),可以自動(dòng)更新依賴的子孫組件。
利用這個(gè)特性,我們可以將需要跨組件共享的狀態(tài)保存在InheritedWidget中,然后在子組件中引用InheritedWidget即可,F(xiàn)lutter社區(qū)著名的Provider包正是基于這個(gè)思想實(shí)現(xiàn)的一套跨組件狀態(tài)共享解決方案。

Provider
我們通過InheritedWidget實(shí)現(xiàn)的思路來一步步的實(shí)現(xiàn)一個(gè)最小功能的Provider。

首先,我們需要一個(gè)保存需要共享的數(shù)據(jù)InheritedWidget,由于具體業(yè)務(wù)數(shù)據(jù)不可預(yù)期,為了通用性,我們使用泛型,定義一個(gè)通用的Inheritedprovider類,它繼承自InheritedWidget:

// 一個(gè)通用的InheritedWidget,保存任需要跨組件共享的狀態(tài)
class InheritedProvider<T> extends InheritedWidget {
  InheritedProvider({@required this.data, Widget child}) : super(child: child);

  //共享狀態(tài)使用泛型
  final T data;

  @override
  bool updateShouldNotify(InheritedProvider<T> old) {
    //在此簡(jiǎn)單返回true,則每次更新都會(huì)調(diào)用依賴其的子孫節(jié)點(diǎn)的`didChangeDependencies`。
    return true;
  }
}

數(shù)據(jù)保存的地方就有了,那么接下來我們需要做的就是在數(shù)據(jù)發(fā)生變化的時(shí)候來重新構(gòu)建InheritedProvider,那么現(xiàn)在就有兩個(gè)問題了:

  1. 數(shù)據(jù)發(fā)生變化怎么通知?
  2. 誰來重新構(gòu)建InheritedProvider?

第一個(gè)問題其實(shí)很好解決,我們可以使用eventBus來進(jìn)行事件通知,但是為了更貼近Flutter開發(fā),我們使用 ChangeNotifier類,它繼承自 Listenable,也實(shí)現(xiàn)了一個(gè)Flutter風(fēng)格的發(fā)布者-訂閱者模式,ChangeNotifier定義大致如下:

class ChangeNotifier implements Listenable {

  @override
  void addListener(VoidCallback listener) {
     //添加監(jiān)聽器
  }
  @override
  void removeListener(VoidCallback listener) {
    //移除監(jiān)聽器
  }

  void notifyListeners() {
    //通知所有監(jiān)聽器,觸發(fā)監(jiān)聽器回調(diào)   
  }

  ... //省略無關(guān)代碼
}

我們可以通過調(diào)用addListener()和removeListener()來添加、移除監(jiān)聽器(訂閱者);通過調(diào)用notifyListeners()可以觸發(fā)所有監(jiān)聽器回調(diào)。

現(xiàn)在,我們將要共享的狀態(tài)放到一個(gè)Model類中,然后讓它繼承自ChangeNotifier,這樣當(dāng)共享的狀態(tài)改變時(shí),我們只需要調(diào)用notifyListeners()來通知訂閱者,然后由訂閱者來重新構(gòu)建InheritedProvider,這也是第二個(gè)問題的答案。
接下來我們便實(shí)現(xiàn)這個(gè)訂閱者類:

// 該方法用于在Dart中獲取模板類型
Type _typeOf<T>() => T;

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  ChangeNotifierProvider({
    Key key,
    this.data,
    this.child,
  });

  final Widget child;
  final T data;

  //定義一個(gè)便捷方法,方便子樹中的widget獲取共享數(shù)據(jù)
  static T of<T>(BuildContext context) {
    final type = _typeOf<InheritedProvider<T>>();
    final provider =  context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>;
    return provider.data;
  }

  @override
  _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}

該類繼承StatefulWidget,然后定義了一個(gè)of()靜態(tài)方法供子類方便獲取Widget樹中的InheritedProvider中保存的共享狀態(tài)model,下面我們實(shí)現(xiàn)該類對(duì)應(yīng)的_ChangenotifierProviderState類:

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
  void update() {
    //如果數(shù)據(jù)發(fā)生變化(model類調(diào)用了notifyListeners),重新構(gòu)建InheritedProvider
    setState(() => {});
  }

  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    //當(dāng)Provider更新時(shí),如果新舊數(shù)據(jù)不"==",則解綁舊數(shù)據(jù)監(jiān)聽,同時(shí)添加新數(shù)據(jù)監(jiān)聽
    if (widget.data != oldWidget.data) {
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  void initState() {
    // 給model添加監(jiān)聽器
    widget.data.addListener(update);
    super.initState();
  }

  @override
  void dispose() {
    // 移除model的監(jiān)聽器
    widget.data.removeListener(update);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

可以看到_ChangeNotifierProviderState類的主要作用就是監(jiān)聽到共享狀態(tài)(model)改變時(shí)重新構(gòu)建Widget樹,此時(shí)會(huì)更新InheritedProvider的data數(shù)據(jù),然后由于InheritedProvider繼承自InheritedWidget,所以所有依賴InheritedProvider的data數(shù)據(jù)的子widget都會(huì)調(diào)用didUpdateDependence方法,然后調(diào)用build方法,達(dá)到狀態(tài)共享的作用。

注意:在_ChangeNotifierProviderState類中調(diào)用 setState() 方法,widget.child 始終是同一個(gè),所以執(zhí)行build時(shí),InheritedProvider的child引用的始終是同一個(gè)子widget,所以widget.child并不會(huì)重新build,這也就相當(dāng)于對(duì)child進(jìn)行了緩存。當(dāng)然如果ChangeNotifierProvider父級(jí)Widget重新build時(shí),則其傳入的child便有可能會(huì)發(fā)生變化。

下面,我們通過一個(gè)購(gòu)物車的例子,來使用一下:
我們需要實(shí)現(xiàn)一個(gè)顯示購(gòu)物車中所有商品總價(jià)的功能:

  1. 向購(gòu)物車中添加新商品時(shí)總價(jià)更新
    定義一個(gè)Item類,用于表示商品信息:
class Item {
  Item(this.price, this.count);
  double price; //商品單價(jià)
  int count; // 商品份數(shù)
  //... 省略其它屬性
}

定義一個(gè)保存購(gòu)物車內(nèi)商品數(shù)據(jù)的CartModel類:

class CartModel extends ChangeNotifier {
  // 用于保存購(gòu)物車中商品列表
  final List<Item> _items = [];

  // 禁止改變購(gòu)物車?yán)锏纳唐沸畔?  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  // 購(gòu)物車中商品的總價(jià)
  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  // 將 [item] 添加到購(gòu)物車。這是唯一一種能從外部改變購(gòu)物車的方法。
  void add(Item item) {
    _items.add(item);
    // 通知監(jiān)聽器(訂閱者),重新構(gòu)建InheritedProvider, 更新狀態(tài)。
    notifyListeners();
  }
}

CartModel即要跨組件共享的model類。最后我們構(gòu)建示例頁(yè)面:

class ProviderRoute extends StatefulWidget {
  @override
  _ProviderRouteState createState() => _ProviderRouteState();
}

class _ProviderRouteState extends State<ProviderRoute> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ChangeNotifierProvider<CartModel>(
        data: CartModel(),
        child: Builder(builder: (context) {
          return Column(
            children: <Widget>[
              Builder(builder: (context){
                var cart=ChangeNotifierProvider.of<CartModel>(context);
                return Text("總價(jià): ${cart.totalPrice}");
              }),
              Builder(builder: (context){
                print("RaisedButton build"); //在后面優(yōu)化部分會(huì)用到
                return RaisedButton(
                  child: Text("添加商品"),
                  onPressed: () {
                    //給購(gòu)物車中添加商品,添加后總價(jià)會(huì)更新
                    ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
                  },
                );
              }),
            ],
          );
        }),
      ),
    );
  }
}

運(yùn)行后效果如圖:


image.png

每次點(diǎn)擊“添加商品”按鈕,總價(jià)就會(huì)增加20,我們期望的功能實(shí)現(xiàn)了。
對(duì)于實(shí)現(xiàn)這個(gè)功能來說,使用ChangeNotifierProvider的優(yōu)勢(shì)并不明顯,但是如果做一個(gè)購(gòu)物APP呢?由于購(gòu)物車數(shù)據(jù)是通常會(huì)在整個(gè)APP中共享的,比如會(huì)跨路由共享。如果我們將ChangeNotifierProvider放在整個(gè)應(yīng)用的Widget樹的根上,那么整個(gè)APP就可以共享購(gòu)物車的數(shù)據(jù)了,這時(shí)ChangeNotifierProvider的優(yōu)勢(shì)將會(huì)很明顯。

雖然上面的例子比較簡(jiǎn)單,但是卻清楚的展現(xiàn)了Provider的原理和流程:


image.png

從上圖我們可以看到 狀態(tài)改變 和 更新UI 是鏈接這三個(gè)組件的部分,而這狀態(tài)改變是通過ChangeNotifier實(shí)現(xiàn)的,更新UI就是通過InheritedWidget的特性實(shí)現(xiàn)的。

Model變化后會(huì)自動(dòng)通知(調(diào)用notifyListeners()方法)通知ChangeNotifierProvider(訂閱者),ChangeNotifierProvider內(nèi)部會(huì)重新構(gòu)建 InheritedWidget,而依賴該 InheritedWidget的子孫Widget就會(huì)更新。
我們可以發(fā)現(xiàn)使用Provider,將會(huì)帶來如下收益:

  1. 我們的業(yè)務(wù)代碼更關(guān)注數(shù)據(jù)了,只要更新Model,則UI會(huì)自動(dòng)更新,而不用再狀態(tài)改變后再去手動(dòng)調(diào)用setState() 來顯式更新頁(yè)面
  2. 數(shù)據(jù)改變的消息傳遞被屏蔽了,我們無需手動(dòng)去處理狀態(tài)改變事件的發(fā)布和訂閱了,這一切都被風(fēng)狀態(tài)Provider中了。
  3. 在大型復(fù)雜應(yīng)用中,尤其是需要全局共享的狀態(tài)非常多時(shí),使用Provider將會(huì)大大簡(jiǎn)化我們的代碼邏輯,降低出錯(cuò)的概率,提高開發(fā)效率。

優(yōu)化
我們上面實(shí)現(xiàn)的ChangeNotifierProvider有兩個(gè)明顯的缺點(diǎn):

  • 代碼組織問題
  • 性能問題

代碼組織問題
我們先看一下構(gòu)建顯示總價(jià)Text的代碼:

Builder(builder: (context){
  var cart=ChangeNotifierProvider.of<CartModel>(context);
  return Text("總價(jià): ${cart.totalPrice}");
})

這段代碼有兩點(diǎn)可以優(yōu)化:

  1. 需要顯示調(diào)用ChangeNotifierProvider.of,當(dāng)APP內(nèi)部依賴CartModel很多時(shí),這樣的代碼很冗余
  2. 語義不明確;由于ChangeNotifierProvider是訂閱者,那么依賴CartModel的Widget自然就是訂閱者,也就是狀態(tài)的消費(fèi)者,如果我們用Builder來構(gòu)建,語義就不是很明確,如果我們能使用一個(gè)具有明確語義的Widget,比如就叫Consumer,這樣的最終代碼語義就會(huì)很明確,只要看到Consumer,我們就知道它是依賴某個(gè)跨組件或全局的狀態(tài)。

為了優(yōu)化這兩個(gè)問題,我們可以封裝一個(gè)Consumer Widget,實(shí)現(xiàn)如下:

class Consumer<T> extends StatelessWidget {

  Consumer({
    Key key,
    @required this.builder,
    this.child
  }): assert(builder != null),
      super(key:key);

  final Widget child;
  final Widget Function(BuildContext context, T value) builder;

  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      ChangeNotifierProvider.of<T>(context), // 自動(dòng)獲取model
    );
  }
}

Consumer實(shí)現(xiàn)很簡(jiǎn)單,它通過指定模板參數(shù),然后再內(nèi)部自動(dòng)調(diào)用ChangeNotifierProvider.of 獲取相應(yīng)的Model,并且Consumer這個(gè)名字也是具有確切的語義(消費(fèi)者)?,F(xiàn)在上面的代碼塊可以優(yōu)化為如下這樣:

Consumer<CartModel>(
  builder: (context, cart)=> Text("總價(jià): ${cart.totalPrice}");
)

性能問題
上面的代碼還有一個(gè)性能問題,就是在構(gòu)建“添加按鈕”的代碼處:

Builder(builder: (context) {
  print("RaisedButton build"); // 構(gòu)建時(shí)輸出日志
  return RaisedButton(
    child: Text("添加商品"),
    onPressed: () {
      ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
    },
  );
}

我們點(diǎn)擊“添加商品”按鈕后,由于購(gòu)物車商品總價(jià)會(huì)變化,所以顯示總價(jià)的Test更新時(shí)符合預(yù)期的,但是“添加商品”按鈕本身是沒有變化,不應(yīng)該被重新build的。
但是我們運(yùn)行示例,每次點(diǎn)擊“添加商品”按鈕,控制臺(tái)都會(huì)輸出RaisedButton build 日止,也就是說“添加商品”按鈕在每次點(diǎn)擊時(shí)其自身都會(huì)重新build。這是為什么呢?
如果你已經(jīng)理解了InheritedWIdget的更新機(jī)制,那么你會(huì)很明白:
因?yàn)闃?gòu)建RaisedButton 的 Builder 中調(diào)用了 ChangeNotifierprovider.of,也就是說依賴了Widget樹上面的InheritedWidget(即InheritedProvider)Widget,所以添加完商品后,CartModel發(fā)生變化,會(huì)通知ChangeNotifierProvider,而ChangeNotifierProvider則會(huì)重新構(gòu)建子樹,所以InheritedProvider將會(huì)更新,此時(shí)依賴它的子孫Widget就會(huì)被重新構(gòu)建。

問題很清楚了,那么我們?nèi)绾伪苊膺@種不必要的build?
既然按鈕重新被build是因?yàn)榘粹o和InheritedWidget建立了依賴關(guān)系,那么我們只需要打破或解除這種依賴關(guān)系就可以了。
如何解除按鈕和InheritedWidget的依賴關(guān)系呢?
我們知道調(diào)用inheritFromWidgetOfExactType() 和 ancestorInheritedElementForWidgetOfExactType()的區(qū)別就是前者會(huì)注冊(cè)依賴關(guān)系,而后者不會(huì),所以我們只需要將ChangeNotifierprovider.of 的實(shí)現(xiàn)改為下面這樣就行:

 //添加一個(gè)listen參數(shù),表示是否建立依賴關(guān)系
  static T of<T>(BuildContext context, {bool listen = true}) {
    final type = _typeOf<InheritedProvider<T>>();
    final provider = listen
        ? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
        : context.ancestorInheritedElementForWidgetOfExactType(type)?.widget
            as InheritedProvider<T>;
    return provider.data;
  }

然后我們將調(diào)用部分代碼改為:

Column(
    children: <Widget>[
      Consumer<CartModel>(
        builder: (BuildContext context, cart) =>Text("總價(jià): ${cart.totalPrice}"),
      ),
      Builder(builder: (context) {
        print("RaisedButton build");
        return RaisedButton(
          child: Text("添加商品"),
          onPressed: () {
            // listen 設(shè)為false,不建立依賴關(guān)系
            ChangeNotifierProvider.of<CartModel>(context, listen: false)
                .add(Item(20.0, 1));
          },
        );
      })
    ],
  )

修改后再次運(yùn)行,我們會(huì)發(fā)現(xiàn)點(diǎn)擊“添加商品”按鈕后,控制臺(tái)不會(huì)再輸出“RaisedButton build”,即按鈕不會(huì)被重新構(gòu)建了。而總價(jià)仍然會(huì)更新,這是因?yàn)镃onsumer中調(diào)用ChangeNotifierProvider.of時(shí)listen值為默認(rèn)值true,所以還是會(huì)建立依賴關(guān)系。

至此我們便實(shí)現(xiàn)了一個(gè)迷你的Provider,它具備Pub上Provider Package中的核心功能;但是我們的迷你版功能并不全面,如只實(shí)現(xiàn)了一個(gè)可監(jiān)聽的ChangeNotifierProvider,并沒有實(shí)現(xiàn)只用于數(shù)據(jù)共享的Provider;另外,我們的實(shí)現(xiàn)有些邊界也沒有考慮的到,比如如何保證在Widget樹重新build時(shí)Model始終是單例等。實(shí)現(xiàn)這個(gè)迷你Provider的主要目的主要是為了幫助了解Provider Package底層的原理,可以自己去看下provider的源碼。

其他狀態(tài)管理包

  • Provider & Scoped Model
    這兩個(gè)包都是基于InheritedWidget的,原理相似
  • Redux
    是Web開發(fā)中React生態(tài)鏈中Redux包的Flutter實(shí)現(xiàn)
  • MobX
    是Web開發(fā)中React生態(tài)鏈中MobX包的Flutter實(shí)現(xiàn)
  • BLoC
    是BLoC模式的Flutter實(shí)現(xiàn)

實(shí)現(xiàn)思想很重要。

總結(jié):
通過介紹事件總線在跨組件共享中的一些缺點(diǎn)引出了通過使用InheritedWidget來實(shí)現(xiàn)狀態(tài)共享的思想,然后基于該思想實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Provider,在實(shí)現(xiàn)的過程中也深入探索了InheritedWidget與其依賴項(xiàng)的注冊(cè)機(jī)制和更新機(jī)制。

InheritedWidget是Flutter中非常重要的一個(gè)Widget,像國(guó)際化、主題等都是通過它來實(shí)現(xiàn),所以要搞懂它。

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

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

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