Flutter | 状态管理指南篇——Provider

本文于 2019.7.8 日更新,修正了关于数据初始化以及 保证 build 函数无反作用这两部分的错误,若文章还存在任何问题,请联系我修复它。git

前言

2019 Google I/O 大会,官方在 Pragmatic State Management in Flutter (Google I/O'19) 主题演讲上正式介绍了 由社区做者 Remi Rousselet 与 Flutter Team 共同编写的 Provider 代替 Provide 成为官方推荐的状态管理方式之一。github

读者老朋友应该都知道,在以前的文章中我介绍了 Google 官方仓库下的一个状态管理 Provide。乍一看这俩玩意可能很容易就被认为是同一个东西,仔细一看,这不就差了一个字吗,有什么区别呢。🧐算法

首先,你要知道的最大的一个区别就是,Provide 被 Provider 干掉了...假如你就是用了 Provide 的幸运鹅,你的心里应该已经开始 甘霖* 这不是坑爹吗 🤦‍♀️。我也在这先给这部分朋友说声抱歉嗷,毕竟不少人是看了我以前那篇文章才入坑的。不过幸运的是,你要从 Provide 迁移到 Provider 并非太难。api

本文将基于最新 Provider v-3.0 进行介绍,除了讲解其使用方式以外,我认为更重要的是 Provider 不一样“提供”方式的适用场景及使用原则。以及在使用状态管理时候须要遵照的原则,在编写 Flutter App 的过程当中减轻你的思考负担。但愿本文能给你带来一些有价值的参考。(提早打个预防针,本文篇幅较长,建议马住在看。)bash

推荐阅读时间:1小时服务器

What's the problem

在正式介绍 Provider 以前容许我再啰嗦两句,为何咱们须要状态管理。若是你已经对此十分清楚,那么建议直接跳过这一节。markdown

若是咱们的应用足够简单,Flutter 做为一个声明式框架,你或许只须要将 数据 映射成 视图 就能够了。你可能并不须要状态管理,就像下面这样。网络

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

WTF,这是什么鬼。咱们很难再清楚的测试维护咱们的状态,由于它看上去实在是太复杂了!并且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展现的时候,外部也须要显示点赞数,这时候就须要同步这两个状态。app

Flutter 实际上在一开始就为咱们提供了一种状态管理方式,那就是 StatefulWidget。可是咱们很快发现,它正是形成上述缘由的罪魁祸首

在 State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可使用 callback 解决,可是当嵌套足够深的话,咱们增长很是多可怕的垃圾代码。

这时候,咱们便迫切的须要一个架构来帮助咱们理清这些关系,状态管理框架应运而生。

What is Provider

那么咱们该如何解决上面这种糟糕的状况呢。在上手这个库以后我能够说 Provider 是一个至关不错的解决方案。(你上次介绍 Provide 也这么说😒)咱们先来简单说一下 Provider 的基本做用。

Provider 从名字上就很容易理解,它就是用于提供数据,不管是在单个页面仍是在整个 app 都有它本身的解决方案,咱们能够很方便的管理状态。能够说,Provider 的目标就是彻底替代 StatefulWidget。

说了不少仍是很抽象,咱们先一块儿作一个最简单的例子。

How to do

这里咱们仍是用这个 Counter App 为例,给你们介绍如何在两个独立的页面中共享计数器(counter)的状态应该怎么作,具体长这样。

两个页面中心字体共用了同一个字体大小。第二个页面的按钮将会让数字增长,第一个页面的数字将会同步增长。

第一步:添加依赖

在pubspec.yaml中添加Provider的依赖。

第二步:建立数据 Model

这里的 Model 实际上就是咱们的状态,它不只储存了咱们的数据模型,并且还包含了更改数据的方法,并暴露出它想要暴露出的数据。

import 'package:flutter/material.dart';

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get value => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}
复制代码

这个类意图很是清晰,咱们的数据就是一个 int 类型的 _count,下划线表明私有。经过 get value_count 值暴露出来。并提供 increment 方法用于更改数据。

这里使用了 mixin 混入了 ChangeNotifier,这个类可以帮驻咱们自动管理全部听众。当调用 notifyListeners() 时,它会通知全部听众进行刷新。

若是你对 mixin 这个概念还不是很清楚的话,能够看我以前翻译的这篇 【译】Dart | 什么是Mixin

第三步:建立顶层共享数据

咱们在 main 方法中初始化全局数据。

void main() {
  final counter = CounterModel();
  final textSize = 48;

  runApp(
    Provider<int>.value(
      value: textSize,
      child: ChangeNotifierProvider.value(
        value: counter,
        child: MyApp(),
      ),
    ),
  );
}
复制代码

经过 Provider<T>.value 可以管理一个恒定的数据,并提供给子孙节点使用。咱们只须要将数据在其 value 属性中声明便可。在这里咱们将 textSize 传入。

ChangeNotifierProvider<T>.value 不只可以提供数据供子孙节点使用,还能够在数据改变的时候通知全部听众刷新。(经过以前咱们说过的 notifyListeners)

此处的 <T> 范型可省略。可是我建议你们仍是进行声明,这会使你的应用更加健壮。

除了上述几个属性以外 Provider<T>.value 还提供了 UpdateShouldNotify Function,用于控制刷新时机。

typedef UpdateShouldNotify<T> = bool Function(T previous, T current);

咱们能够在这里传入一个方法 (T previous, T current){...} ,并得到先后两个 Model 的实例,而后经过比较两个 Model 以自定义刷新规则,返回 bool 表示是否须要刷新。默认为 previous != current 则刷新。

固然,key 属性是确定有的,常规操做。若是你还不太清楚的话,建议阅读我以前的这篇文章 [Flutter | 深刻浅出Key] (juejin.im/post/5ca215…)

为了让各位思惟连贯,我仍是在这里放上这个平淡无奇的 MyApp Widget 代码。😑

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: FirstScreen(),
    );
  }
}
复制代码

第四步:在子页面中获取状态

在这里咱们有两个页面,FirstScreen 和 SecondScreen。咱们先来看 FirstScreen 的代码。

Provider.of(context)

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _counter = Provider.of<CounterModel>(context);
    final textSize = Provider.of<int>(context).toDouble();

    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Text(
          'Value: ${_counter.value}',
          style: TextStyle(fontSize: textSize),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => SecondPage())),
        child: Icon(Icons.navigate_next),
      ),
    );
  }
}
复制代码

获取顶层数据最简单的方法就是 Provider.of<T>(context); 这里的范型 <T> 指定了获取 FirstScreen 向上寻找最近的储存了 T 的祖先节点的数据。

咱们经过这个方法获取了顶层的 CounterModel 及 textSize。并在 Text 组件中进行使用。

floatingActionButton 用来点击跳转到 SecondScreen 页面,和咱们的主题无关。

Consumer

看到这里你可能会想,两个页面都是获取顶层状态,代码不都同样吗,弄啥捏。🤨 别忙着跳到下一节,咱们来看另一种获取状态的方式,这将会影响你的 app performance。

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Consumer2<CounterModel,int>(
        builder: (context, CounterModel counter, int textSize, _) => Center(
              child: Text(
                'Value: ${counter.value}',
                style: TextStyle(
                  fontSize: textSize.toDouble(),
                ),
              ),
            ),
      ),
      floatingActionButton: Consumer<CounterModel>(
        builder: (context, CounterModel counter, child) => FloatingActionButton(
              onPressed: counter.increment,
              child: child,
            ),
        child: Icon(Icons.add),
      ),
    );
  }
}
复制代码

这里咱们要介绍的是第二种方式,使用 Consumer 获取祖先节点中的数据。

在这个页面中,咱们有两处使用到了公共 Model。

  • 应用中心的文字:使用 CounterModel 在 Text 中展现文字,以及经过 textSize 定义自身的大小。一共使用到了两个 Model。
  • 浮动按钮:使用 CounterModel 的 increment 方法触发计数器的值增长。使用到了一个 Model。

Single Model Consumer

咱们先看 floatingActionButton,使用了一个 Consumer 的状况。

Consumer 使用了 Builder 模式,收到更新通知就会经过 builder 从新构建。Consumer<T> 表明了它要获取哪个祖先中的 Model。

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

  • context: context 就是 build 方法传进来的 BuildContext 在这里就不细说了,若是有兴趣能够看我以前这篇文章 Flutter | 深刻理解BuildContext
  • T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
  • child:它用来构建那些与 Model 无关的部分,在屡次运行 builder 中,child 不会进行重建。

而后它会返回一个经过这三个参数映射的 Widget 用于构建自身。

在这个浮动按钮的例子中,咱们经过 Consumer 获取到了顶层的 CounterModel 实例。并在浮动按钮 onTap 的 callback 中调用其 increment 方法。

并且咱们成功抽离出 Consumer 中不变的部分,也就是浮动按钮中心的 Icon 并将其做为 child 参数传入 builder 方法中。

Consumer2

如今咱们再来看中心的文字部分。这时候你可能会有疑惑了,刚才咱们讲的 Consumer 获取的只有一个 Model,而如今 Text 组件不只须要 CounterModel 用以显示计数器,并且还须要得到 textSize 以调整字体大小,咋整捏。

遇到这种状况你可使用 Consumer2<A,B>。使用方式基本上和 Consumer<T> 一致,只不过范型改成了两个,而且 builder 方法也变成了 Function(BuildContext context, A value, B value2, Widget child)

我勒个去...假如我要得到 100 个 Model,那岂不是得搞个 Consumer100 (???黑人问号.jpg)

然而并无 😏。

从源码里面能够看到,做者只为咱们搞到了 Consumer6。emmmmm.....还要要求更多就只有自力更生喽。

顺手帮做者修复了一个 clerical error。

区别

咱们来看 Consumer 的内部实现。

@override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }
复制代码

能够发现,Consumer 就是经过 Provider.of<T>(context) 来实现的。可是从实现来说 Provider.of<T>(context)Consumer 简单好用太多,为啥我要搞得那么复杂捏。

实际上 Consumer 很是有用,它的经典之处在于可以在复杂项目中,极大地缩小你的控件刷新范围Provider.of<T>(context) 将会把调用了该方法的 context 做为听众,并在 notifyListeners 的时候通知其刷新。

举个例子来讲,咱们的 FirstScreen 使用了 Provider.of<T>(context) 来获取数据,SecondScreen 则没有。

  • 你在 FirstScreen 中的 build 方法中添加一个 print('first screen rebuild');
  • 而后在 SecondScreen 中的 build 方法中添加一个 print('second screen rebuild');
  • 点击第二个页面的浮动按钮,那么你会在控制台看到这句输出。

first screen rebuild

首先这证实了 Provider.of<T>(context) 会致使调用的 context 页面范围的刷新。

那么第二个页面刷新没有呢? 刷新了,可是只刷新了 Consumer 的部分,甚至连浮动按钮中的 Icon 的不刷新咱们都给控制了。你能够在 Consumer 的 builder 方法中验证,这里再也不啰嗦

假如你在你的应用的 页面级别 的 Widget 中,使用了 Provider.of<T>(context)。会致使什么后果已经显而易见了,每当其状态改变的时候,你都会从新刷新整个页面。虽然你有 Flutter 的自动优化算法给你撑腰,但你确定没法得到最好的性能

因此在这里我建议各位尽可能使用 Consumer 而不是 Provider.of<T>(context) 获取顶层数据。

以上即是一个最简单的使用 Provider 的例子。

You also need to know

合理选择使用 Provides 的构造方法

在上面这个例子中👆,咱们选择了使用 XProvider<T>.value 的构造方法来建立祖先节点中的 提供者。除了这种方式,咱们还可使用默认构造方法。

Provider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  }) : this._(
          key: key,
          delegate: BuilderStateDelegate<T>(builder, dispose: dispose),
          updateShouldNotify: null,
          child: child,
        );
复制代码

常规的 key/child 属性咱们不在这里啰嗦。咱们先来看这个看上去相对教复杂一点的 builder。

ValueBuilder

相比起 .value 构造方式中直接传入一个 value 就 ok,这里的 builder 要求咱们传入一个 ValueBuilder。WTF?

typedef ValueBuilder<T> = T Function(BuildContext context);

其实很简单,就是传入一个 Function 返回一个数据而已。在上面这个例子中,你能够替换成这样。

Provider(
    builder: (context) => textSize,
    ...
)
复制代码

因为是 Builder 模式,这里默认须要传入 context,实际上咱们的 Model(textSize)与 context 并无关系,因此你彻底能够这样写。

Provider(
    builder: (_) => textSize,
    ...
)
复制代码

Disposer

如今咱们知道了 builder,那这个 dispose 方法又用来作什么的呢。实际上这才是 Provider 的点睛之笔。

typedef Disposer<T> = void Function(BuildContext context, T value);

dispose 属性须要一个 Disposer<T>,而这个其实也是一个回调。

若是你以前使用过 BLoC 的话,相信你确定遇到过一个头疼的问题。我应该在何时释放资源呢? BloC 使用了观察者模式,它旨在替代 StatefulWidget。然而大量的流使用完毕以后必须 close 掉,以释放资源。

然而 Stateless Widget 并无给咱们相似于 dispose 之类的方法,这即是 BLoC 的硬伤。你不得不为了释放资源而使用 StatefulWidget,这与咱们的本意相违。而 Provider 则为咱们解决了这一点。

当 Provider 所在节点被移除的时候,它就会启动 Disposer<T>,而后咱们即可以在这里释放资源。

举个例子,假如咱们有这样一个 BLoC。

class ValidatorBLoC {
  StreamController<String> _validator = StreamController<String>.broadcast();

  get validator => _validator.stream;

  validateAccount(String text) {
    //Processing verification text ...
  }

  dispose() {
    _validator.close();
  }
}
复制代码

这时候咱们想要在某个页面提供这个 BLoC 可是又不想使用 StatefulWidget。这时候咱们能够在页面顶层套上这个 Provider。

Provider(
    builder:(_) => ValidatorBLoC(),
    dispose:(_, ValidatorBLoC bloc) => bloc.dispose(),
    }
)
复制代码

这样就完美解决了数据释放的问题!🤩

如今咱们能够放心的结合 BLoC 一块儿使用了,很赞有没有。可是如今你可能又有疑问了,在使用 Provider 的时候,我应该选择哪一种构造方法呢。

个人推荐是,简单模型就选择 Provider<T>.value,好处是能够精确控制刷新时机。而须要对资源进行释放处理等复杂模型的时候,Provider() 默认构造方式绝对是你的最佳选择。

其余几种 Provider 也遵循该模式,须要的时候能够自行查看源码。

我该使用哪一种 Provider

若是你在 Provider 中提供了可监听对象(Listenable 或者 Stream)及其子类的话,那么你会获得下面这个异常警告。

你能够将本文中所使用到的 CounterModel 放入 Provider 进行提供(记得 hot restart 而不是 hot reload),那么你就能看到上面这个 FlutterError 了。

你也能够在 main 方法中经过下面这行代码来禁用此提示。 Provider.debugCheckInvalidValueType = null;

这是因为 Provider 只能提供恒定的数据,不能通知依赖它的子部件刷新。提示也说的很清楚了,假如你想使用一个会发生 change 的 Provider,请使用下面的 Provider。

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

你可能会在这里产生一个疑问,不是说(Listenable 或者 Stream)才不行吗,为何咱们的 CounterModel 混入的是 ChangeNotifier 可是仍是出现了这个 FlutterError 呢。

class ChangeNotifier implements Listenable

咱们再来看上面的这几个 Provider 有什么异同。先关注 ListenableProvider / ChangeNotifierProvider 这两个类。

ListenableProvider 提供(provide)的对象是继承了 Listenable 抽象类的子类。因为没法混入,因此经过继承来得到 Listenable 的能力,同时必须实现其 addListener / removeListener 方法,手动管理收听者。显然,这样太过复杂,咱们一般都不须要这样作。

而混入了 ChangeNotifier 的类自动帮咱们实现了听众管理,因此 ListenableProvider 一样也能够接收混入了 ChangeNotifier 的类。

ChangeNotifierProvider 则更为简单,它可以对子节点提供一个 继承 / 混入 / 实现 了 ChangeNotifier 的类。一般咱们只须要在 Model 中 with ChangeNotifier ,而后在须要刷新状态的时候调用 notifyListeners 便可。

那么 ChangeNotifierProviderListenableProvider 究竟区别在哪呢,ListenableProvider 不是也能够提供(provide)混入了 ChangeNotifier 的 Model 吗。

仍是那个你须要思考的问题。你在这里的 Model 到底是一个简单模型仍是复杂模型。这是由于 ChangeNotifierProvider 会在你须要的时候,自动调用其 _disposer 方法。

static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose();

咱们能够在 Model 中重写 ChangeNotifier 的 dispose 方法,来释放其资源。这对于复杂 Model 的状况下十分有用。

如今你应该已经十分清楚 ListenableProvider / ChangeNotifierProvider 的区别了。下面咱们来看 ValueListenableProvider。

ValueListenableProvider 用于提供实现了 继承 / 混入 / 实现 了 ValueListenable 的 Model。它其实是专门用于处理只有一个单一变化数据的 ChangeNotifier。

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>

经过 ValueListenable 处理的类再也不须要数据更新的时候调用 notifyListeners

好了,终于只剩下最后一个 StreamProvider 了。

StreamProvider 专门用做提供(provide)一条 Single Stream。我在这里仅对其核心属性进行讲解。

  • T initialData:你能够经过这个属性声明这条流的初始值。
  • ErrorBuilder<T> catchError:这个属性用来捕获流中的 error。在这条流 addError 了以后,你会可以经过 T Function(BuildContext context, Object error) 回调来处理这个异常数据。实际开发中它很是有用。
  • updateShouldNotify:和以前的回调同样,这里再也不赘述。

除了这三个构造方法都有的属性之外,StreamProvider 还有三种不一样的构造方法。

  • StreamProvider(...):默认构造方法用做建立一个 Stream 并收听它。
  • StreamProvider.controller(...):经过 builder 方式建立一个 StreamController<T>。而且在 StreamProvider 被移除时,自动释放 StreamController。
  • StreamProvider.value(...):监听一个已有的 Stream 并将其 value 提供给子孙节点。

除了上面这五种已经提到过的 Provider,还有一种 FutureProvider,它提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新,这里再也不详细介绍,须要的话自行查看 api 文档。

优雅地处理多个 Provider

在咱们以前的例子中,咱们使用了嵌套的方式来组合多个 Provider。这样看上去有些傻瓜(我就是有一百个 Model 🙃)。

这时候咱们就可使用一个很是 sweet 的组件 —— MultiProvider

这时候咱们刚才那个例子就能够改为这样。

void main() {
  final counter = CounterModel();
  final textSize = 48;

  runApp(
    MultiProvider(
      providers: [
        Provider.value(value: textSize),
        ChangeNotifierProvider.value(value: counter)
      ],
      child: MyApp(),
    ),
  );
}
复制代码

咱们的代码瞬间清晰不少,并且与刚才的嵌套作法彻底等价。

Tips

保证 build 方法无反作用

build 无反作用也一般被人叫作,build 保持 pure,两者是一个意思。

一般咱们常常会看到,为了获取顶层数据咱们会在 build 方法中调用 XXX.of(context) 方法。你必须很是当心,你的 build 函数不该该产生任何反作用,包括新的对象(Widget 之外),请求网络,或做出一个映射视图之外的操做等。

这是由于,你的根本没法控制何时你的 build 函数将会被调用。我能够说随时。每当你的 build 函数被调用,那么都会产生一个反作用。这将会发生很是恐怖的事情。🤯

我这样说你确定会感到比较抽象,咱们来举一个例子。

假如你有一个 ArticleModel 这个 Model 的做用是 经过网络 获取一页 List 数据,并用 ListView 显示在页面上。

这时候,咱们假设你在 build 函数中作了下面这些事情。

@override
  Widget build(BuildContext context) {
      final articleModel = Provider.of<ArticleModel>(context);
      mainCategoryModel.getPage(); // By requesting data from the server
      return XWidget(...);
  }
复制代码

咱们在 build 函数中得到了祖先节点中的 articleModel,随后调用了 getPage 方法。

这时候会发生什么事情呢,当咱们请求成功得到告终果的时候,根据以前咱们已经介绍过的,调用了 Provider.of<T>(context); 会从新运行其 build。这样 getPage 就又被执行了一次。

而你的 Model 中每次请求 getPage 都会致使 Model 中保存的当前请求页自增(第一次请求第一页的数据,第二次请求第二页的数据以此类推),那么每次 build 都会致使新的一次数据请求,并在新的数据 get 的时候请求下一页的数据。你的服务器挂掉那是早晚的事情。(come on baby!

因为 didChangeDependence 方法也会随着依赖改变而被调用,因此也须要保证它没有反作用。具体解释参见下面单页面数据初始化。

因此你应该严格遵照这项原则,不然会致使一系列糟糕的后果。

那么怎么解决数据初始化这个问题呢,请看 Q&A 部分。

不要全部状态都放在全局

第二个小贴士是不要把你的全部状态都放在顶层。开发者为了图方便省事,再接触了状态管理以后常常喜欢把全部东西都放在顶层 MaterialApp 之上。这样看上去就很方便共享数据了,我要数据就直接去获取。

不要这么作。严格区分你的全局数据与局部数据,资源不用了就要释放!不然将会严重影响你的应用 performance。

尽可能在 Model 中使用私有变量“_”

这多是咱们每一个人在新手阶段都会出现的疑问。为何要用私有变量呢,我在任何地方都可以操做成员不是很方便吗。

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

虽然两种方式的效果彻底同样,可是第二种方式将会让咱们的business logic零散的混入其余代码中。长此以往项目中就会大量充斥着这些垃圾代码增长项目代码耦合程度,很是不利于代码的维护以及阅读。

因此,请务必使用私有变量保护你的 Model。

控制你的刷新范围

在 Flutter 中,组合大于继承的特性随处可见。常见的 Widget 实际上都是由更小的 Widget 组合而成,直到基本组件为止。为了使咱们的应用拥有更高的性能,控制 Widget 的刷新范围便显得相当重要。

咱们已经经过前面的介绍了解到了,在 Provider 中获取 Model 的方式会影响刷新范围。全部,请尽可能使用 Consumer 来获取祖先 Model,以维持最小刷新范围。

Q&A

在这里对一些你们可能会有疑问的常见问题作一个回答,若是你还有这以外的疑问的话,欢迎在下方评论区一块儿讨论。

Provider 是如何作到状态共享的

这个问题实际上得分两步。

获取顶层数据

实际上在祖先节点中共享数据这件事咱们已经在以前的文章中接触过不少次了,都是经过系统的 InheritedWidget 进行实现的。

Provider 也不例外,在全部 Provider 的 build 方法中,返回了一个 InheritedProvider。

class InheritedProvider<T> extends InheritedWidget

Flutter 经过在每一个 Element 上维护一个 InheritedWidget 哈希表来向下传递 Element 树中的信息。一般状况下,多个 Element 引用相同的哈希表,而且该表仅在 Element 引入新的 InheritedWidget 时改变。

因此寻找祖先节点的时间复杂度为 O(1) 😎

通知刷新

通知刷新这一步实际上在讲各类 Provider 的时候已经讲过了,其实就是使用了 Listener 模式。Model 中维护了一堆听众,而后 notifiedListener 通知刷新。(空间换时间🤣

为何全局状态须要放在顶层 MaterialApp 之上

这个问题须要结合 Navigator 以及 BuildContext 来回答,在以前的文章中 Flutter | 深刻理解BuildContext 已经解释过了,这里再也不赘述。

我应该在哪里进行数据初始化

对于数据初始化这个问题,咱们必需要分类讨论。

全局数据

当咱们须要获取全局顶层数据(就像以前 CounterApp 例子同样)并须要作一些会产生额外结果的时候,main 函数是一个很好的选择。

咱们能够在 main 方法中建立 Model 并进行初始化的工做,这样就只会执行一次。

单页面

若是咱们的数据只是在这个页面中须要使用,那么你有这两种方式能够选择。

StatefulWidget

这里订正一个错误,感谢 @晓杰的V笑 以及 @fantasy525 在讨论中帮我指出。

在以前文章的版本中我推荐你们在 State 的 didChangeDependence 中进行数据初始化。这里实际上是使用 BLoC 延续下来的习惯。由于使用了 InheritWidget 以后,只有在 State 的 didChangeDependence 阶段进行 Inherit 初始化,initState 阶段是拿不到数据的。而因为 BLoC 是使用的 Stream,数据直接走 Stream 进来,由 StreamBuilder 去 listen,这样 State 的依赖一直都只是这个 Stream 对象而已,不会再次触发 didChangeDependence 方法。那 Provider 有何不一样呢。

/// If [listen] is `true` (default), later value changes will trigger a new
  /// [State.build] to widgets, and [State.didChangeDependencies] for
  /// [StatefulWidget].
复制代码

源码中的注释解释了,若是这个 Provider.of<T>(context) listen 了的话,那么当 notifyListeners 的时候,就会触发 context 所对应的 State 的 [State.build] 和 [State.didChangeDependencies] 方法。也就是说,若是你使用了非 Provider 提供的数据,例如 ChangeNotifierProvider 这样会改变依赖的类,而且获取数据时 Provider.of<T>(context, listen: true) 选择 listen (默认就为 listen)的话,数据刷新时会从新运行 didChangeDependencies 和 build 两个方法。这样一来对 didChangeDependencies 也会产生反作用。假如在这里请求了数据,当数据到来的时候,又回触发下一次请求,最终无限请求下去。

这里除了反作用之外还有一点,假如数据改变是一个同步行为,例如这里的 counter.increment 这样的方法,在 didChangeDependencies 中调用的话,就会形成下面这个错误。

The following assertion was thrown while dispatching notifications for CounterModel:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<CounterModel> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. This exception is
flutter: allowed because the framework builds parent widgets before children, which means a dirty descendant
flutter: will always be built. Otherwise, the framework might not visit this widget during this build phase.
复制代码

这里和和 Flutter 的构建算法有关。简单来讲,就是不可以在 State 的 build 期间调用 setState() 或者 markNeedsBuild(),在咱们这里 didChangeDependence 的时候调用了此方法,致使出现这个错误。异步数据则会因为 event loop 的缘故不会当即执行。想要深刻了解的同窗能够看闲鱼技术的这篇文章:Flutter快速上车之Widget

感受到处都是坑啊,那该怎么初始化呢。目前我找到的办法是这样,首先 要保证初始化数据不可以产生反作用,咱们须要找一个在 State 声明周期内必定只会运行一次的方法。initState 就是为此而生的。可是 initState 不是没法获取到 Inherit 吗。可是咱们如今自己就在页面顶层啊,页面级别的 Model 就在顶层被建立,如今根本就不须要 Inherit。

class _HomeState extends State<Home> {
    final _myModel = MyModel();
    
      @override
  void initState() {
    super.initState();
    _myModel.init(); 
  }
}
复制代码

页面级别的 Model 数据都在页面顶层 Widget 建立并初始化便可。

咱们还须要考虑一种状况,假如这个操做是一个同步操做应该如何处理,就如咱们以前举的 CounterModel.increment 这个操做同样。

void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((callback){
      Provider.of<CounterModel>(context).increment();
    });
  }
复制代码

咱们经过 addPostFrameCallback 回调中在第一帧 build 结束时调用 increment 方法,这样就不会出现构建错误了。

provider 做者 Remi 给出了另一种方式

This code is relatively unsafe. There's more than one reason for didChangeDependencies to be called.

You probably want something similar to:

MyCounter counter;

@override
void didChangeDependencies() {
  final counter = Provider.of<MyCounter>(context);
  if (conter != this.counter) {
    this.counter = counter;
    counter.increment();
  }
}
复制代码

This should trigger increment only once.

也就是说初始化数据以前判断一下这个数据是否已经存在。

cascade

你也能够在使用 dart 的级连语法 ..do() 直接在页面的 StatelessWidget 成员变量声明时进行初始化。

class FirstScreen extends StatelessWidget {
    CounterModel _counter = CounterModel()..increment();
    double _textSize = 48;
    ...
}
复制代码

使用这种方式须要注意,当这个 StatelessWidget 从新运行 build 的时候,状态会丢失。这种状况在 TabBarView 中的子页面切换过程当中就可能会出现。

因此建议仍是使用第一种,在 State 中初始化数据。

我须要担忧性能问题吗

是的,不管 Flutter 再怎么努力优化,Provider 考虑的状况再多,咱们老是有办法让应用卡爆 😂(开个玩笑)

仅当咱们不遵照其行为规范的时候,会出现这样的状况。性能会由于你的各类不当操做而变得很糟糕。个人建议是:遵照其规范,作任何事情都考虑对性能的影响,要知道 Flutter 把更新算法但是优化到了 O(N)。

Provider 仅仅是对 InheritedWidget 的一个升级,你没必要担忧引入 Provider 会对应用形成性能问题。

为何选择 Provider

Provider 不只作到了提供数据,并且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数状况。就连 BLoC 未解决的那个棘手的 dispose 问题,和 ScopedModel 的侵入性问题,它也都解决了。

然而它就是完美的吗,并非,至少如今来讲。Flutter Widget 构建模式很容易在 UI 层面上组件化,可是仅仅使用 Provider,Model 和 View 之间仍是容易产生依赖。

咱们只有经过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系,因此假如各位有组件化的需求,还须要另外处理。

不过对于大多数状况来讲,Provider 足以优秀,它可以让你开发出简单高性能层次清晰 的应用。

我应该如何选择状态管理

介绍了这么多状态管理,你可能会发现,一些状态管理之间职责并不冲突。例如 BLoC 能够结合 RxDart 库变得很强大,很好用。而 BLoC 也能够结合 Provider / ScopedModel 一块儿使用。那我应该选择哪一种状态管理方式呢。

个人建议是遵照如下几点:

  1. 使用状态管理的目的是为了让编写代码变得更简单,任何会增长你的应用复杂度的状态管理,通通都不要用。
  2. 选择本身可以 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有必定上手难度,不要选本身没法理解的状态管理方式。
  3. 在作最终决定以前,敲一敲 demo,真正感觉各个状态管理方式给你带来的 好处/坏处 而后再作你的决定。

但愿可以帮助到你。

源码浅析

这里在分享一点源码浅析(真的很浅😅)

Flutter 中的 Builder 模式

在 Provider 中,各类 Provider 的原始构造方法都有一个 builder 参数,这里通常就用 (_) => XXXModel() 就好了。感受有点屡次一举,为何不能像 .value() 构造方法那样简洁呢。

实际上,Provider 为了帮咱们管理 Model,使用到了 delegation pattern。

builder 声明的 ValueBuilder 最终被传入代理类 BuilderStateDelegate / SingleValueDelegate。 而后经过代理类才实现的 Model 生命周期管理。

class BuilderStateDelegate<T> extends ValueStateDelegate<T> {
  BuilderStateDelegate(this._builder, {Disposer<T> dispose})
      : assert(_builder != null),
        _dispose = dispose;
  
  final ValueBuilder<T> _builder;
  final Disposer<T> _dispose;
  
  T _value;
  @override
  T get value => _value;

  @override
  void initDelegate() {
    super.initDelegate();
    _value = _builder(context);
  }

  @override
  void didUpdateDelegate(BuilderStateDelegate<T> old) {
    super.didUpdateDelegate(old);
    _value = old.value;
  }

  @override
  void dispose() {
    _dispose?.call(context, value);
    super.dispose();
  }
}
复制代码

这里就仅放 BuilderStateDelegate,其他的请自行查看源码。

如何实现 MultiProvider

Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }
复制代码

MultiProvider 实际上就是经过每个 provider 都实现了的 cloneWithChild 方法把本身一层一层包裹起来。

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)
复制代码

等价于

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)
复制代码

写在最后

此次写的太顺畅,不当心就写得过多了。能看到这里的朋友,都很强 🤣。

与其说此次是 Provider 专场,更像是把状态管理本身所遇到的心得都总结在这里了。但愿可以给各位有参考价值。

后期的 Tips 和 Q&A 有一部分实际上对大多数状态管理都适用,我后面会考虑把这些专门拉出来说一篇。不过下篇文章主题已经决定了,在 Flutter 中实现无 context 导航 的。若是你感兴趣的话必定不要错过。

若是您对Provider还有任何疑问或者文章的建议,欢迎在下方评论区以及个人邮箱1652219550a@gmail.com与我联系,我会及时回复!

相关文章
相关标签/搜索