Flutter 状态管理之Provider

flutter中状态管理是重中之重,每当谈这个话题,总有说不完的话。

在正式介绍 Provider 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。

image

但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
image

这又是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

Provider 是什么

通过使用Provider而不用手动编写InhertedWidget,您将获取自动分配、延迟加载、大大减少每次创建新类的代码。

首先在yaml中添加,具体版本号参考:官方Provider pub,当前版本号是4.1.3.

  Provider: ^4.1.3

然后运行

flutter pub get

获取到最新的包到本地,在需要的文件夹内导入

import 'package:provider/provider.dart';

简单例子

我们还用点击按钮新增数字的例子

首先创建存储数据的Model

class ProviderModel extends ChangeNotifier {

    int _count=0;
    ProviderModel();
   void plus() {
   /// 在数据变动的时候通知监听者刷新UI
    _count = _count + 1;
    notifyListeners();
  }
}

构造view

/// 使用Consumer来监听全局刷新UI
Consumer<ProviderModel>(
        builder:
            (BuildContext context, ProviderModel value, Widget child) {
          print('Consumer 0 刷新');
          _string += 'c0 ';
          return _Row(
            value: value._count.toString(),
            callback: () {
              context.read<ProviderModel>().plus();
            },
          );
        },
        child: _Row(
          value: '0',
          callback: () {
            context.read<ProviderModel>().plus();
          },
        ),
      )

测试下看下效果:

image

单个Model多个小部件分别刷新(局部刷新)

单个model实现单个页面多个小部件分别刷新,是使用Selector<Model,int>来实现,首先看下构造函数:

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继承了Selector0,再看Selector关键build代码:

class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    var shouldInvalidateCache = oldWidget != widget ||
        (widget._shouldRebuild != null &&
            widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null &&
            !const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    return cache;
  }
}

根据我们传入的_shouldRebuild来判断是否需要更新,如果需要更新则执行widget.build(context,selected,child),否则返回已经缓存的cache.当没有_shouldRebuild参数时则根据widget.selector(ctx)的返回值判断是否和旧值相等,不等则更新UI

所以我们不写shouldRebuild也是可以的。

局部刷新用法

  Widget build(BuildContext context) {
    print('page 1');
    _string += 'page ';
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider 全局与局部刷新'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text('全局刷新<Consumer>'),
            Consumer<ProviderModel>(
              builder:
                  (BuildContext context, ProviderModel value, Widget child) {
                print('Consumer 0 刷新');
                _string += 'c0 ';
                return _Row(
                  value: value._count.toString(),
                  callback: () {
                    context.read<ProviderModel>().plus();
                  },
                );
              },
              child: _Row(
                value: '0',
                callback: () {
                  context.read<ProviderModel>().plus();
                },
              ),
            ),
            SizedBox(
              height: 40,
            ),
            Text('局部刷新<Selector>'),
            Selector<ProviderModel, int>(
              builder: (ctx, value, child) {
                print('Selector 1 刷新');
                _string += 's1 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次数:' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        context.read<ProviderModel>().plus2();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
              selector: (ctx, model) => model._count2,
              shouldRebuild: (m1, m2) {
                print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '数据相等,本次不刷新'}');
                return m1 != m2;
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('局部刷新<Selector>'),
            Selector<ProviderModel, int>(
              selector: (context, model) => model._count3,
              shouldRebuild: (m1, m2) {
                print('s2:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '数据相等,本次不刷新'}');
                return m1 != m2;
              },
              builder: (ctx, value, child) {
                print('selector 2 刷新');
                _string += 's2 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次数:' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        ctx.read<ProviderModel>().plus3();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('刷新次数和顺序:↓'),
            Text(_string),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                OutlineButton(
                  child: Icon(Icons.refresh),
                  onPressed: () {
                    setState(() {
                      _string += '\n';
                    });
                  },
                ),
                OutlineButton(
                  child: Icon(Icons.close),
                  onPressed: () {
                    setState(() {
                      _string = '';
                    });
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }

效果:

image

当我们点击局部刷新s1,执行s1builds1不相等,s2相等不刷新。输出:

flutter: s2:5 5 数据相等,本次不刷新
flutter: s1:6 7 不相等,本次刷新
flutter: Selector 1 刷新
flutter: Consumer 0 刷新

当点击s2,s2的值不相等刷新UI,s1数据相等,不刷新UI.

flutter: s2:2 3 不相等,本次刷新
flutter: selector 2 刷新
flutter: s1:0 0 数据相等,本次不刷新
flutter: Consumer 0 刷新

可以看到上边2次Consumer每次都刷新了,我们探究下原因。

Consumer 全局刷新

Consumer继承了SingleCHildStatelessWidget,当我们在ViewModel中调用notification则当前widget被标记为dirty,然后在build中执行传入的builder函数,在下帧则会刷新UI

Selector<T,S>则被标记dirty时执行_Selector0State中的buildWithChild(ctx,child)函数时,根据selected_shouldRebuild来判断是否需要执行widget.builder(ctx,selected,child)(刷新UI).

其他用法

多model写法

只需要在所有需要model的上级包裹即可,当我们一个page需要2model的时候,我么通常这样子写:

class BaseProviderRoute extends StatelessWidget {
  BaseProviderRoute({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<ProviderModel>(
          create: (_) => ProviderModel(),
        ),
        ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()),
      ],
      child: BaseProvider(),
    );
  }
}

当然是用的时候和单一model一致的。

  Selector<ProviderModel2, int>(
    selector: (context, model) => model.value,
    builder: (ctx, value, child) {
      print('model2 s1 刷新');
      _string += 'm2s1 ';
      return Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Selector<Model2,int>次数:' + value.toString()),
          OutlineButton(
            onPressed: () {
              ctx.read<ProviderModel2>().add(2);
            },
            child: Icon(Icons.add),
          )
        ],
      );
    },
  ),

watch && read

watch源码是Provider.of<T>(this),默认Provider.of<T>(this)listen=true.

static T of<T>(BuildContext context, {bool listen = true}){
final inheritedElement = _inheritedElementOf<T>(context);

    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }

    return inheritedElement.value;
}

read源码是Provider.of<T>(this, listen: false),watch/read只是写法简单一点,并无高深结构。

当我们想要监听值的变化则是用watch,当想调用model的函数时则使用read

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342