Flutter 功能型組件:跨組件狀態共享(Provider)

前言

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

  1. 必須顯示定義各類事件,很差管理;
  2. 訂閱者必須顯式註冊狀態改變回調,也必須在組件銷燬時手動去解綁回調以免內存泄漏;

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

Provider

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

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

  爲了通用性,使用泛型。less

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

  存在兩個問題:ide

  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便有可能會發生變化。ui

代碼示例

// 跨組件狀態共享(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));
                    },
                  );
                },)
              ],
            );
          },),
        ),
      ),
    );
  }
}

代碼優化

  兩個地方能夠進行代碼優化,詳細看代碼示例。this

總結

Provider原理圖

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

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

其餘狀態管理包

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

相關文章
相關標籤/搜索