Flutter 全域性狀態管理之 Provider 初探

Flutter程式設計開發發表於2019-09-28

一、什麼是全域性狀態管理

當我們在使用 Flutter 進行應用開發時,可能需要不同的頁面共享應用或者說變數的狀態,當這個狀態發生改變時,所有依賴這個狀態的 ui 都會隨之發生改變。在同一個頁面中還好說,直接通過 setState 就可以達到目的,要是不同的頁面呢,或者當應用變得非常複雜,頁面非常多的時候,這個時候全域性狀態管理就顯得非常重要了。

在 Flutter 中,狀態管理可以有如下幾種方式:

1、setState

flutter 中最簡單使 ui 根據狀態發生改變的方式。

2、 InheritedWidget & InheritedModel

InheritedWidget 和 InheritedModel 是 flutter 原生提供的狀態管理解決方案。 當InheritedWidget發生變化時,它的子樹中所有依賴了它的資料的Widget都會進行rebuild,這使得開發者省去了維護資料同步邏輯的麻煩。

3、Provider & Scoped Model

Provider 與 Scoped Model 都屬於第三方庫,兩者使用起來差不多,其中 Provider 是 Google I/O 2019 大會上官方推薦的狀態管理方式。

4、Redux

在 Redux 狀態管理中,所有的狀態都儲存在Store裡,Flutter 中的 Widget 會根據這個 Store 去渲染檢視,而狀態的改變也是通過 Reduex 裡面的 action 來進行的。

5、BLoC / Rx

BLoC的全稱是 業務邏輯元件(Business Logic Component)。就是用reactive programming方式構建應用,一個由流構成的完全非同步的世界。 BLoc 可以看作是 Flutter 中的非同步事件匯流排,當然在除了 BLoc 外,Flutter 中有專門的響應式程式設計庫,就是RxDart,RxDart是基於ReactiveX標準API的Dart版本實現,由Dart標準庫中Stream擴充套件而成。

二、Provider 介紹和使用

1、Provider 是什麼

Provider 是 Google 官方推薦的狀態管理解決方案,本質上也是使用 InheritedWidget 來進行狀態管理的,所以也可以理解為 Provider 是 Flutter 中的語法糖,主要是對 InheritedWidget 的封裝方便我們的使用。

Provider 使用起來非常方便,訪問資料的方式有兩種,無論是獲取狀態還是更新狀態,都是這兩種:

  • Provider.of(context)
  • Consumer
2、Provider 基本使用

接下來直接參考官方的 Demo 了。

1、要新增依賴:

  provider: ^3.1.0
複製程式碼

2、定義資料 model

class Counter with ChangeNotifier {
  ///這個 model 只管理一個變數。
  int value = 0;

  ///操作變數
  void increment() {
    value += 1;
    notifyListeners();
  }
}
複製程式碼

3、使用 ChangeNotifierProvider 進行資料管理

   ChangeNotifierProvider(
      // Initialize the model in the builder. That way, Provider
      // can own Counter's lifecycle, making sure to call `dispose`
      // when not needed anymore.
      ///builder 會指定資料 model 並初始化。
      builder: (context) => Counter(),
      child: MyApp(),
    ),
複製程式碼

4、監聽狀態改變可以使用 Provider.of 或者 Consumer


            Consumer<Counter>(
              builder: (context, counter, child) => Text(
                '${counter.value}',
                style: Theme.of(context).textTheme.display1,
              ),
            ),

            Text('使用 Provider.of 方式 獲取 model:'),
            Text('${_counter.value}',),
複製程式碼

5、改變資料,同樣可以使用 Provider.of 或者 Consumer

      floatingActionButton: FloatingActionButton(
        /// listen 為 false 表示不監聽狀態改變,預設時 true
        onPressed: () => Provider.of<Counter>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),


      ///需要修改 Model 同樣可以使用 Consumer 的方式
//        floatingActionButton: Consumer<Counter>(
//          builder: (context, Counter counter, child) => FloatingActionButton(
//            onPressed: counter.increment,
//            child: child,
//          ),
//          child: Icon(Icons.add),
//        ),


複製程式碼

完整程式碼:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ///使用 ChangeNotifierProvider ,這個 Provider 將資料 model 粘合在一起,資料改變時,保證 MyApp 或者其子 Widget ui 更新。
    ChangeNotifierProvider(
      // Initialize the model in the builder. That way, Provider
      // can own Counter's lifecycle, making sure to call `dispose`
      // when not needed anymore.
      ///builder 會指定資料 model 並初始化。
      builder: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

/// Simplest possible model, with just one field.
///
/// [ChangeNotifier] is a class in `flutter:foundation`. [Counter] does
/// _not_ depend on Provider.
///
///
class Counter with ChangeNotifier {
  ///這個 model 只管理一個變數。
  int value = 0;

  ///操作變數
  void increment() {
    value += 1;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    ///通過 Provider.of 方式獲取 model
    final _counter = Provider.of<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('使用 Consumer 獲取 model:'),
            // Consumer looks for an ancestor Provider widget
            // and retrieves its model (Counter, in this case).
            // Then it uses that model to build widgets, and will trigger
            // rebuilds if the model is updated.

            ///Consumer 迴向上尋找 Provider 型別的父類 Widget,並且取出 Provider 關聯的 Model,根據這個 model 來構建 widget
            ///並且當 model 資料發生改變時,回觸發更新。
            ///

            Consumer<Counter>(
              builder: (context, counter, child) => Text(
                '${counter.value}',
                style: Theme.of(context).textTheme.display1,
              ),
            ),

            Text('使用 Provider.of 方式 獲取 model:'),
            Text('${_counter.value}',),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        /// listen 為 false 表示不監聽狀態改變,預設時 true
        onPressed: () => Provider.of<Counter>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),


      ///需要修改 Model 同樣可以使用 Consumer 的方式
//        floatingActionButton: Consumer<Counter>(
//          builder: (context, Counter counter, child) => FloatingActionButton(
//            onPressed: counter.increment,
//            child: child,
//          ),
//          child: Icon(Icons.add),
//        ),


    );
  }
}

複製程式碼

效果:

Flutter 全域性狀態管理之 Provider 初探

3、多個頁面資料共享

接下來看一下如何在不同的頁面中共享資料做到全域性狀態管理。

還是以官方 Demo 為例說明。 假設有一個購物應用程式,有一個購物車頁面和一個結賬頁面,購物車頁面新增商品,結賬頁面可以看到所有新增的商品,對商品來說,就是共享的資料來源。 先看一下效果:

Flutter 全域性狀態管理之 Provider 初探

1、定義資料 model

在這個示例中,有兩個 model,一個是商品模型,一個是購物車,購物車存放選擇的商品。

  • 商品 Model
/// A proxy of the catalog of items the user can buy.
///
/// In a real app, this might be backed by a backend and cached on device.
/// In this sample app, the catalog is procedurally generated and infinite.
///
/// For simplicity, the catalog is expected to be immutable (no products are
/// expected to be added, removed or changed during the execution of the app).
class CatalogModel {
  static const _itemNames = [
    'Code Smell',
    'Control Flow',
    'Interpreter',
    'Recursion',
    'Sprint',
    'Heisenbug',
    'Spaghetti',
    'Hydra Code',
    'Off-By-One',
    'Scope',
    'Callback',
    'Closure',
    'Automata',
    'Bit Shift',
    'Currying',
  ];

  /// Get item by [id].
  ///
  /// In this sample, the catalog is infinite, looping over [_itemNames].
  Item getById(int id) => Item(id, _itemNames[id % _itemNames.length]);

  /// Get item by its position in the catalog.
  Item getByPosition(int position) {
    // In this simplified case, an item's position in the catalog
    // is also its id.
    return getById(position);
  }
}

@immutable
class Item {
  final int id;
  final String name;
  final Color color;
  final int price = 42;

  Item(this.id, this.name)
      // To make the sample app look nicer, each item is given one of the
      // Material Design primary colors.
      : color = Colors.primaries[id % Colors.primaries.length];

  @override
  int get hashCode => id;

  @override
  bool operator ==(Object other) => other is Item && other.id == id;
}

複製程式碼

只是簡單的模擬一下使用者選擇的商品,包含 id,name,color,price 這四個欄位。

  • 購物車 Model
class CartModel extends ChangeNotifier {
  /// The current catalog. Used to construct items from numeric ids.
  final CatalogModel _catalog;

  /// 購物車中存放商品的 list,只存 id 就行
  final List<int> _itemIds;

  /// Construct a CartModel instance that is backed by a [CatalogModel] and
  /// an optional previous state of the cart.
  ///
  /// If [previous] is not `null`, it's items are copied to the newly
  /// constructed instance.
  CartModel(this._catalog, CartModel previous)
      : assert(_catalog != null),
        _itemIds = previous?._itemIds ?? [];



  /// 將存放商品 id 的陣列轉換為存放商品的數值,函數語言程式設計。
  List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();

  /// 獲取價格總和,dart 中的 List 中有兩個累加的方法 reduce 和 fold,fold 可以提供一個初始值。
  int get totalPrice =>
      items.fold(0, (total, current) => total + current.price);

  ///新增商品,這個方法時外界可以修改 list 的唯一途徑
  void add(Item item) {
    _itemIds.add(item.id);
    // This line tells [Model] that it should rebuild the widgets that
    // depend on it.
    notifyListeners();
  }
}
複製程式碼

商品中通過 List 存放選擇的商品。這裡的購物車 Model 實現的是 ChangeNotifier,做為可改變的資料來源。對於不同型別的可改變資料來源,Provider 提供了不同的類提供我們選擇,常見的有如下幾種:

Flutter 全域性狀態管理之 Provider 初探

2、購物車頁面提供商品選擇,改變資料狀態

class MyCatalog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          _MyAppBar(),
          ///上間距
          SliverToBoxAdapter(child: SizedBox(height: 12)),
          SliverList(
            delegate: SliverChildBuilderDelegate(
                (context, index) => _MyListItem(index)
              ,
                childCount: 25      ///本來時無限載入的,這裡加上數量限制。
            ),
          ),
        ],
      ),
    );
  }
}

class _AddButton extends StatelessWidget {
  final Item item;

  const _AddButton({Key key, @required this.item}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    ///通過 Provider.of 方式使用 CartModel
    var cart = Provider.of<CartModel>(context);

    return FlatButton(
      ///判斷是否為空,不為空 list 中新增 item
      onPressed: cart.items.contains(item) ? null : () => cart.add(item),
      splashColor: Theme.of(context).primaryColor,
      child: cart.items.contains(item)
          ? Icon(Icons.check, semanticLabel: 'ADDED')
          : Text('ADD'),
    );
  }
}

class _MyAppBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverAppBar(
      title: Text('Catalog', style: Theme.of(context).textTheme.display4),
      floating: true,
      actions: [
        IconButton(
          icon: Icon(Icons.shopping_cart),
          onPressed: () => Navigator.pushNamed(context, '/cart'),
        ),
      ],
    );
  }
}

class _MyListItem extends StatelessWidget {
  final int index;

  _MyListItem(this.index, {Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    ///Provider.of 方式獲取 model
    var catalog = Provider.of<CatalogModel>(context);
    var item = catalog.getByPosition(index);
    var textTheme = Theme.of(context).textTheme.title;

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: LimitedBox(
        maxHeight: 48,
        child: Row(
          children: [
            AspectRatio(
              aspectRatio: 1,
              child: Container(
                color: item.color,
              ),
            ),
            SizedBox(width: 24),
            Expanded(
              child: Text(item.name, style: textTheme),
            ),
            SizedBox(width: 24),
            _AddButton(item: item),
          ],
        ),
      ),
    );
  }
}

複製程式碼

3、購物車頁面,獲取資料


class MyCart extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Cart', style: Theme.of(context).textTheme.display4),
        backgroundColor: Colors.white,
      ),
      body: Container(
        color: Colors.yellow,
        child: Column(
          children: [
            Expanded(
              child: Padding(
                padding: const EdgeInsets.all(32),
                child: _CartList(),
              ),
            ),
            Divider(height: 4, color: Colors.black),
            ///價格
            _CartTotal()
          ],
        ),
      ),
    );
  }
}

class _CartList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var itemNameStyle = Theme.of(context).textTheme.title;

    ///使用 Provider.of 方式獲取 CartModel
    var cart = Provider.of<CartModel>(context);

    return ListView.builder(
      itemCount: cart.items.length,
      itemBuilder: (context, index) => ListTile(
        leading: Icon(Icons.done),
        title: Text(
          cart.items[index].name,
          style: itemNameStyle,
        ),
      ),
    );
  }
}

class _CartTotal extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var hugeStyle = Theme.of(context).textTheme.display4.copyWith(fontSize: 48);

    return SizedBox(
      height: 200,
      child: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ///使用 Consumer 方式使用 CartModel
            Consumer<CartModel>(
                builder: (context, cart, child) =>
                    Text('\$${cart.totalPrice}', style: hugeStyle)),
            SizedBox(width: 24),
            FlatButton(
              onPressed: () {
                Scaffold.of(context).showSnackBar(
                    SnackBar(content: Text('Buying not supported yet.')));
              },
              color: Colors.white,
              child: Text('BUY'),
            ),
          ],
        ),
      ),
    );
  }
}

複製程式碼

github

參考:

https://medium.com/flutter-community/flutter-statemanagement-with-provider-ee251bbc5ac1
https://flutter.cn/docs/development/data-and-backend/state-mgmt/simple
https://pub.dev/packages/provider#-readme-tab-
https://pub.dev/documentation/provider/latest/provider/provider-library.html
複製程式碼

最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

Flutter 全域性狀態管理之 Provider 初探

相關文章