【Flutter】功能型組件之跨組件狀態共享


前言

  在Flutter開發中,狀態管理是一個永恆的話題。
  一般的原則是:如果狀態是組件私有的,則應該由組件自己管理;如果狀態要跨組件共享,則該狀態應該由各個組件共同的父元素來管理。
  對於組件私有的狀態管理很好理解,但對於跨組件共享的狀態,管理的方式就比較多了,如使用全局事件總線EventBus,它是一個觀察者模式的實現,通過它就可以實現跨組件狀態同步:狀態持有方(發布者)負責更新、發布狀態,狀態使用方(觀察者)監聽狀態改變事件來執行一些操作。
  但是觀察者模式來實現跨組件狀態共享有一些明顯的缺點:

  1. 必須顯示定義各種事件,不好管理;
  2. 訂閱者必須顯式注冊狀態改變回調,也必須在組件銷毀時手動去解綁回調以避免內存泄漏;

  是否還有更好的跨組件狀態管理方式了?
  我們知道,InheritedWidget能綁定與它依賴的子孫組件的依賴關系,並且當InheritedWidget數據發生變化時,可以自動更新依賴的子孫組件。基於此,可以將需要跨組件共享的狀態保存在InheritedWidget中,然后在子組件中引用InheritedWidget即可。Flutter社區的Provider包就是基於這個思想實現的。

Provider

  基於上面的思想,實現一個最小功能的Provider。

定義一個保存共享數據的類

  為了通用性,使用泛型。

class InheritedProvider<T> extends InheritedWidget{
  InheritedProvider({@required this.data, Widget child}): super(child: child);

  // 共享狀態使用泛型
  final T data;

  bool updateShouldNotify(InheritedProvider<T> old){
    // 返回true,則每次更新都會調用依賴其的子孫節點的didChangeDependencies
    return true;
  }
}

數據發生變化時如何重構InheritedProvider

  存在兩個問題:

  1. 數據發生變化怎么通知?
  2. 誰來重新構建InheritedProvider?

  對於第一個問題,可以使用之前介紹的eventBus來進行事件通知,但是為了更貼近Flutter開發,使用Flutter中SDK中提供的ChangeNotifier類 ,它繼承自Listenable,也實現了一個Flutter風格的發布者-訂閱者模式,可以通過調用addListener()和removeListener()來添加、移除監聽器(訂閱者);通過調用notifyListeners() 可以觸發所有監聽器回調。
  對於第二個問題,將要共享的狀態放到一個Model類中,然后讓它繼承自ChangeNotifier,這樣當共享的狀態改變時,我們只需要調用notifyListeners() 來通知訂閱者,然后由訂閱者來重新構建InheritedProvider。

// 該方法用於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;

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

  // 替換上面的便捷方法,按需求是否注冊依賴關系
  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;
  }

  _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();

}


// _ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。
// 注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,
// 所以執行build時,InheritedProvider的child引用的始終是同一個子widget,
// 所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
  void update(){
    // 如果數據發生變化(model類調用了notifyListeners),重新構建InheritedProvider
    setState(() => {});
  }

  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
    // 當Provider更新時,如果新舊數據不相等,則解綁舊數據監聽,同時添加新數據監聽
    if(widget.data != oldWidget.data){
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

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

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

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

}

  可以看到_ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,所以執行build時,InheritedProvider的child引用的始終是同一個子widget,所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。

代碼示例

// 跨組件狀態共享(Provider)


// 一個通用的InheritedWidget,保存任需要跨組件共享的狀態
import 'dart:collection';

import 'package:flutter/material.dart';

class InheritedProvider<T> extends InheritedWidget{
  InheritedProvider({@required this.data, Widget child}): super(child: child);

  // 共享狀態使用泛型
  final T data;

  bool updateShouldNotify(InheritedProvider<T> old){
    // 返回true,則每次更新都會調用依賴其的子孫節點的didChangeDependencies
    return true;
  }
}


// 該方法用於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;

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

  // 替換上面的便捷方法,按需求是否注冊依賴關系
  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;
  }

  _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();

}


// _ChangeNotifierProviderState類的主要作用就是監聽到共享狀態(model)改變時重新構建Widget樹。
// 注意,在_ChangeNotifierProviderState類中調用setState()方法,widget.child始終是同一個,
// 所以執行build時,InheritedProvider的child引用的始終是同一個子widget,
// 所以widget.child並不會重新build,這也就相當於對child進行了緩存!當然如果ChangeNotifierProvider父級Widget重新build時,則其傳入的child便有可能會發生變化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
  void update(){
    // 如果數據發生變化(model類調用了notifyListeners),重新構建InheritedProvider
    setState(() => {});
  }

  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
    // 當Provider更新時,如果新舊數據不相等,則解綁舊數據監聽,同時添加新數據監聽
    if(widget.data != oldWidget.data){
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

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

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

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

}


// 購物車示例:實現一個顯示購物車中所有商品總價的功能

// 用於表示商品信息
class Item{
  // 商品單價
  double price;
  // 商品份數
  int count;

  Item(this.price, this.count);

}


// 保存購物車內商品數據,跨組件共享
class CartModel extends ChangeNotifier{
  // 用於保存購物車中商品列表
  final List<Item> _items = [];

  // 禁止改變購物車里的商品信息
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

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

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

}


// 優化
// 一個便捷類,封裝一個Consumer的Widget
class Consumer<T> extends StatelessWidget{
  final Widget child;
  final Widget Function(BuildContext context, T value) builder;

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

  Widget build(BuildContext context){
    return builder(
      context,
      // 自動獲取Model
      ChangeNotifierProvider.of<T>(context),
    );
  }
}


// 頁面
class ProviderRoute extends StatefulWidget{
  _ProviderRouteState createState() => _ProviderRouteState();
}

class _ProviderRouteState extends State<ProviderRoute>{
  @override
  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('跨組件狀態共享(Provider)'),
      ),
      body: 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("總價:${cart.totalPrice}");
//                },),

              // 進行優化,替換上面Builder
                Consumer<CartModel>(
                  builder: (context, cart) => Text("總價:${cart.totalPrice}"),
                ),

                Builder(builder: (context){
                  // 控制台打印出這句,說明按鈕在每次點擊時其自身都會重新build!
                  print("RaisedButton build");
                  return RaisedButton(
                    child: Text("添加商品"),
                    onPressed: (){
                      // 給購物車中添加商品,添加后總價會更新
//                      ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
                      // listen設為false,不建立依賴關系,因為按鈕不需要每次重新build
                      ChangeNotifierProvider.of<CartModel>(context, listen: false).add(Item(20.0, 1));
                    },
                  );
                },)
              ],
            );
          },),
        ),
      ),
    );
  }
}

代碼優化

  兩個地方可以進行代碼優化,詳細看代碼示例。

總結

Provider原理圖


  Model變化后會自動通知ChangeNotifierProvider(訂閱者),ChangeNotifierProvider內部會重新構建InheritedWidget,而依賴該InheritedWidget的子孫Widget就會更新。
  可以發現使用Provider,將會帶來如下收益:

  1. 業務代碼更關注數據了,只要更新Model,則UI會自動更新,而不用在狀態改變后再去手動調用setState()來顯式更新頁面。
  2. 數據改變的消息傳遞被屏蔽了,我們無需手動去處理狀態改變事件的發布和訂閱了,這一切都被封裝在Provider中了。這真的很棒,幫我們省掉了大量的工作!
  3. 在大型復雜應用中,尤其是需要全局共享的狀態非常多時,使用Provider將會大大簡化我們的代碼邏輯,降低出錯的概率,提高開發效率。

其他狀態管理包

  Provider & Scoped Model、Redux、MobX、BLoC,這里特別推薦阿里咸魚團隊推出的開源項目:fish redux。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM