Flutter 状态管理(Provider)

12,088 阅读7分钟

状态管理这个概念刚开始看的时候不太懂什么意思,研究一下发现其实状态管理就是我们原生中的数据管理

Flutter是声明式的,这意味着Flutter是通过更新UI来反映当前app的状态。简单来说,在Flutter中,如果我们想更新我们的控件,最基本的方式应该是setState()了。

202006031126379701.png

如果说我们一个页面里的组件不多,直接使用setState()并没有什么问题,但是实际工作中,我们的页面布局还是足够复杂的。 一种情况是我们在一个页面中:

202006031126381908.png

如果我们把所有的Widget都写到一个类里,而且很容易陷入{{{{}}}}旋涡。这时我们会想到Widget进行拆分,但这个时候如果仅仅依靠setState(),你会发现这将十分痛苦,因为setState()的作用域仅限于当作Widget,也就是说如果你仅仅在最底层的Widget里调用setState并不会更新顶层的Widget,这就意味你要通过回调实现,而且在这个过程中你会发现一些Widget类里的变量又必须是不可变的,这又会引起其他的麻烦事

随着产品需求迭代节奏加快,项目逐渐变得庞大时,我们往往就需要管理不同组件、不同页面之间共享的数据关系。当需要共享的数据关系达到几十上百个的时候,我们就很难保持清晰的数据流动方向和顺序了,导致应用内各种数据传递嵌套和回调满天飞。在这个时候,我们迫切需要一个解决方案,来帮助我们理清楚这些共享数据的关系,于是状态管理框架便应运而生。

初识Provider

Flutter 在设计声明式 UI 上借鉴了不少 React 的设计思想,因此涌现了诸如 flutter_redux、flutter_mobx 、fish_redux 等基于前端设计理念的状态管理框架。但这些框架大都比较复杂,且需要对框架设计概念有一定理解,学习门槛相对较高。

而源自 Flutter 官方的状态管理框架 Provider 则相对简单得多,不仅容易理解,而且框架的入侵性小,还可以方便地组合和控制 UI 刷新粒度。因此,在 Google I/O 2019 大会一经面世,Provider 就成为了官方推荐的状态管理方式之一。

Provider 是一个用来提供数据的框架。它是 InheritedWidget 的语法糖,提供了依赖注入的功能,允许在 Widget 树中更加灵活地处理和传递数据。

在使用 Provider 之前,我们首先需要在 pubspec.yaml 文件中添加 Provider 的依赖:

dependencies:
  flutter:
    sdk: flutter
  provider: 3.0.0+1  #provider 依赖

1、简单使用

我们做一个简单的案例:第一个界面读取数据,第二个界面读写数据

1、数据状态的封装

添加好 Provider 的依赖后,我们就可以进行数据状态的封装了。这里,我们只有一个状态需要共享,即 count。由于第二个页面还需要修改状态,因此我们还需要在数据状态的封装上包含更改数据的方法:

import 'package:flutter/cupertino.dart';
// 定义需要共享的数据模型,通过混入 ChangeNotifier 管理听众
class CounterModel with ChangeNotifier {
  int _count = 0;
  // 读方法
  int get counter => _count;
  // 写方法
  void increment() {
    _count++;
    notifyListeners();// 通知听众刷新
  }
}

我们在资源封装类中使用 mixin 混入了 ChangeNotifier。这个类能够帮助我们管理所有依赖资源封装类的听众。当资源封装类调用 notifyListeners 时,它会通知所有听众进行刷新。

如果对mixin这个概念还不是很清楚的话,可以参考这篇文章Dart | 什么是Mixin

尽量在 Model 中使用私有变量_

一个应用需要大量开发人员参与,你写的代码也许在几个月之后被另外一个开发看到了,这时候假如你的变量没有被保护的话,也许同样是让 count++,他会用countController.sink.add(++_count) 这种原始方法,而不是调用你已经封装好了的increment方法。

虽然两种方式的效果完全一样,但是第二种方式将会让我们的business logic零散的混入其他代码中。久而久之项目中就会大量充斥着这些垃圾代码增加项目代码耦合程度,非常不利于代码的维护以及阅读。

2、创建顶层共享数据

资源已经封装完毕,接下来我们就需要考虑把它放到哪儿了。

因为 Provider 实际上是 InheritedWidget 的语法糖,所以通过 Provider 传递的数据从数据流动方向来看,是由父到子(或者反过来)。这时我们就明白了,原来需要把资源放到 FirstProviderPage 和 SecondProviderPage 的父 Widget,

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
     // 通过 Provider 组件封装数据资源
    return ChangeNotifierProvider.value(
        value: CounterModel(),// 需要共享的数据资源
        child: MaterialApp(
          home: ...,
        )
    );
  }
}

我们直接在 MaterialApp 的外层使用 Provider 进行包装,就可以把数据资源依赖注入到应用中。

我们也可以写成这个样子

void main() {
  runApp(ChangeNotifierProvider.value(
      value: CounterModel(),// 需要共享的数据资源
      child: MyApp()
  ));
}

3、在FirstProviderPage中读取数据

class _FirstProviderPageState extends State<FirstProviderPage> {
  @override
  Widget build(BuildContext context) {
    // 取出资源
    final _counter = Provider.of<CounterModel>(context);
    return Container(
      child: Column(
        children: [
          // 展示资源中的数据
          Text('Counter: ${_counter.counter}'),
          // 用资源更新方法来设置按钮点击回调
          RaisedButton(
              child: Text('进入二级界面'),
              onPressed: (){
                Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondProviderPage()));
          })
        ],
      ),
    );
  }
}

4、在SecondProviderPage中写入数据

class _SecondProviderPageState extends State<SecondProviderPage> {
  @override
  Widget build(BuildContext context) {
    // 取出资源
    final _counter = Provider.of<CounterModel>(context);
    return Scaffold(
        appBar: AppBar(title: Text('SecondProviderPage'),),
        // 展示资源中的数据
        body: Container(
          child: Column(
            children: [
              Text('Counter: ${_counter.counter}'),
              // 用资源更新方法来设置按钮点击回调
              RaisedButton(
                  onPressed:  _counter.increment,
                  child: Icon(Icons.add))
            ],
          ),
        ),
    );
  }
}

Kapture 2021-03-17 at 10.29.48.gif

2、Consumer

使用 Provider.of 获取资源,可以得到资源暴露的数据的读写接口,在实现数据的共享和同步上还是比较简单的。但是,滥用 Provider.of 方法也有副作用,那就是当数据更新时,页面中其他的子 Widget 也会跟着一起刷新。

我们在SecondProviderPage里面添加一个widget

class TestView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('print TestView build');
    return Container(width: 100,height: 100,color: red,);
  }
}

BA2068CE-CE00-42DF-B921-41AA264E44B5.png

我们点击

flutter: 0
flutter: print TestView build
flutter: 1
flutter: print TestView build
flutter: 2
flutter: print TestView build
flutter: 3
flutter: print TestView build
flutter: 4
flutter: print TestView build
flutter: 5
flutter: print TestView build

当当数据更新时,页面中其他的子 Widget 也会跟着一起刷新,这是严重影响性能的。那么,有没有办法能够在数据资源发生变化时,只刷新对资源存在依赖关系的 Widget,而其他 Widget 保持不变呢?

这时就要引出这个关键字了,Provider 可以精确地控制 UI 刷新粒度,而这一切是基于 Consumer 实现的。Consumer 使用了 Builder 模式创建 UI,收到更新通知就会通过 builder 重新构建 Widget。

接下来我们对SecondProviderPage进行改造

class _SecondProviderPageState extends State<SecondProviderPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SecondProviderPage'),),
      body: Column(
        children: [
          // 使用 Consumer 来封装 counter 的读取
          Consumer<CounterModel>(
            //builder 函数可以直接获取到 counter 参数
            builder: (context,CounterModel count,_) => Text('Counter: ${count.counter}')),

          // 使用 Consumer 来封装 increment 的读取
          Consumer<CounterModel>(
            //builder 函数可以直接获取到 increment 参数
            builder: (context, CounterModel counter, child) => RaisedButton(
            onPressed: counter.increment,
            child: Icon(Icons.add))),
          TestView(),
        ])
    );
  }
}

我们点击

flutter: 0
flutter: 1
flutter: 2
flutter: 3
flutter: 4
flutter: 5
flutter: 6
flutter: 7

Consumer 中的 builder 实际上就是真正刷新 UI 的函数

Consumer 的 builder 实际上就是一个 Function,它接收三个参数 (BuildContext context, T model, Widget child)。

final Widget Function(BuildContext context, T value, Widget child) builder
  • context: context 就是 build 方法传进来的 BuildContext

  • T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。

  • child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。

当我们的Consumer有多个参数的时候,可以调用不同的Consumer

10C948CC-09DD-4D90-BD62-AB02C47D824F.png

3、Selector

总得来说,Selector和Consumer是等价的,也是通过Provider.of获取数据的,不同的是,Selector正如他的名字一样,他会过滤掉一些不必要的数据更新从而阻止重新构建,也就是说Selector只会更新符合条件的数据。

我们先看一下Selector的定义:

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}
  

先解释一下Selector<A, S>中的泛型:

  • A是我们从顶层获取的Provider的类型
  • S是我们关心的具体类型,也就是获取到的Provider中真正对我们有用的类型,需要在selector 中返回该类型。这个Selector的刷新范围也从整个Provider变成了 S。

快速地看一下Selector的中的属性:

  • selector:就是一个Function,入参会将我们获取的顶层 provider传入,然后再返回我们所关心的S。
  • shouldRebuild:这个属性会储存selector过滤后的值,也就是selector返回的S 并拿收到通知之后新的S与缓存的S进行比较,以此来判断这个Selector是否需要重新构建,默认preview!=next就刷新,如果是collection,selector进行深度比较。
  • builder:和Consumer一样,这里返回的是要构建的控件,第二个参数provider,就是我们刚才selector中返回的S。
  • child:这个用于优化一些不用刷新的部分,之前我们说Consumer的时候也有说过。

1、简单实现一个商品列表的样例

class GoodsListProvider with ChangeNotifier {
  List<Goods> _goodsList =
  List.generate(10, (index) => Goods(false, 'Goods No. $index'));

  get goodsList => _goodsList;
  get total => _goodsList.length;

  collect(int index) {
    var good = _goodsList[index];
    _goodsList[index] = Goods(!good.isCollection, good.goodsName);
    notifyListeners();
  }
}

class Goods{
  bool isCollection;
  String goodsName;
  Goods(this.isCollection,this.goodsName);
}

我们简单给商品两个属性,一个是 isCollection 这个代表是否收藏该商品,另一个是 goodsName,代表商品名字。然后我们实现了 GoodsListProvider 来提供数据。其中 collect 方法可以收藏/取消 index 位置上的商品。

2、通过Selector过滤刷新时机

class GoodsListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => GoodsListProvider(),
      child: ,
    );
  }
}
  • 1、我们还是需要在这个页面顶层通过ChangeNotifierProvider提供数据
  • 2、我们在 create 中创建刚刚建好的 GoodsListProvider
typedef Create<T> = T Function(BuildContext context);

3、实现列表界面

然后我们可以开始实现列表,很明显要实现这个列表我们就需要知道列表有多长,这个 total 是作用于整个列表的,但是我们又不希望它因为列表中某个 item 变化了就刷新,所以我们现在通过 Selector 实现一个不刷新的 “Consumer”,也就是过滤掉全部刷新。

    child: Selector<GoodsListProvider, GoodsListProvider>(
        shouldRebuild: (pre, next) => false,
        selector: (context, provider) => provider,
        builder: (context, provider, child) {
          return ListView.builder(
            itemCount: provider.total,
            itemBuilder: (context, index) {}

我并不想要这个 Selector 刷新,因为如果这个 Selector 刷新了,等于整个列表都刷新了,这正是我们想避免的,所以这里 shouldRebuild 我返回了 false

4、创建itemView

itemBuilder: (context, index) {
              return Selector<GoodsListProvider, Goods>(
                selector: (context, provider) => provider.goodsList[index],
                builder: (context, data, child) {
                  print(('No.${index + 1} rebuild'));
                  return ListTile(
                    title: Text(data.goodsName),
                    trailing: GestureDetector(
                      onTap: () => provider.collect(index),
                      child: Icon(
                          data.isCollection ? Icons.star : Icons.star_border),
                    ),
                  );
                },
              );
            },

ListView 这里就是使用了刚刚获取的 provider 中的 total 构建这个列表。然后列表中的每一个商品我们希望它根据自己的状态进行刷新,所以这时候就需要再次使用 Selector 来获取我们真正关心的那个 Good。

我们可以看到这里的 selector 返回了 provider.goodsList[index] 也就是具体的一个商品信息,所以每个商品只关注自己那部分信息,那么这个 Selector 的刷新范围就是 该商品。

80878845-BF0A-48A1-857F-266D73C6ECD1.png

4、多状态管理MultiProvider

我们继续使用第一个案例,点击加号不到count加1,文本的字体颜色也进行一次改变

添加一个ColorModel

class ColorModel with ChangeNotifier {
  Color _color = Colors.red;
  // 读方法
  Color get color => _color;
  // 写方法
  void randomColor() {
    _color = Color.fromARGB(
        255, 
        Random().nextInt(256)+0, 
        Random().nextInt(256)+0, 
        Random().nextInt(256)+0);
    notifyListeners();// 通知听众刷新
  }
}

main函数修改

void main() {
  runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider.value(value: ColorModel()),
          ChangeNotifierProvider.value(value: CounterModel())],
        child: MyApp(),
  ));
}

SecondProviderPage界面

class _SecondProviderPageState extends State<SecondProviderPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SecondProviderPage'),),
      body: Column(
        children: [
          // 使用 Consumer 来封装 counter 的读取
          Consumer2<CounterModel,ColorModel>(
            //builder 函数可以直接获取到 counter 参数
            builder: (context,CounterModel count,ColorModel colormodel,_) {
              return Text('Counter: ${count.counter}',
                style: TextStyle(color: colormodel.color),);
            }),

          // 使用 Consumer 来封装 increment 的读取
          Consumer2<CounterModel,ColorModel>(
            //builder 函数可以直接获取到 increment 参数
            builder: (context, CounterModel counter, ColorModel colormodel,child) => RaisedButton(
            onPressed: (){
              counter.increment();
              colormodel.randomColor();
            },
            child: Icon(Icons.add))),
          TestView(),
        ])
    );
  }
}

Kapture 2021-03-17 at 16.27.33.gif

参考