阅读 1504

Flutter 全局状态管理之 Provider 初探

一、什么是全局状态管理

当我们在使用 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),
//        ),


    );
  }
}

复制代码

效果:

3、多个页面数据共享

接下来看一下如何在不同的页面中共享数据做到全局状态管理。

还是以官方 Demo 为例说明。 假设有一个购物应用程序,有一个购物车页面和一个结账页面,购物车页面添加商品,结账页面可以看到所有添加的商品,对商品来说,就是共享的数据源。 先看一下效果:

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 提供了不同的类提供我们选择,常见的有如下几种:

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 编程开发」微信公众号 。