Flutter完整开发实战详解(十5、全面理解State与Provider)

本篇将带你深刻理解 Flutter 中 State 的工做机制,并经过对状态管理框架 Provider 解析加深理解,看完这一篇你将更轻松的理解你的 “State 大后宫” 。前端

前文:git

⚠️第十二篇中更多讲解状态的是管理框架,本篇更多讲解 Flutter 自己的状态设计。 github

1、State

一、State 是什么?

咱们知道 Flutter 宇宙中万物皆 Widget ,而 Widget@immutable 即不可变的,因此每一个 Widget 状态都表明了一帧。面试

在这个基础上, StatefulWidgetState 帮咱们实现了在 Widget 的跨帧绘制 ,也就是在每次 Widget 重绘的时候,经过 State 从新赋予 Widget 须要的绘制信息。redux

二、State 怎么实现跨帧共享?

这就涉及 Flutter 中 Widget 的实现原理,在以前的篇章咱们介绍过,这里咱们说两个涉及的概念:bash

  • Flutter 中的 Widget 在通常状况下,是须要经过 Element 转化为 RenderObject 去实现绘制的。app

  • ElementBuildContext 的实现类,同时 Element 持有 RenderObjectWidget咱们代码中的 Widget build(BuildContext context) {} 方法,就是被 Element 调用的。框架

了解这个两个概念后,咱们先看下图,在 Flutter 中构建一个 Widget ,首先会建立出这个 WidgetElement而事实上 State 实现跨帧共享,就是将 State 保存在Element 中,这样 Element 每次调用 Widget build() 时,是经过 state.build(this); 获得的新 Widget ,因此写在 State 的数据就得以复用了。less

State 是在哪里被建立的?ide

以下图所示,StatefulWidgetcreateState 是在 StatefulElement 的构建方法里建立的, 这就保证了只要 Element 不被从新建立,State 就一直被复用。

同时咱们看 update 方法,当新的 StatefulWidget 被建立用于更新 UI 时,新的 widget 就会被从新赋予到 _state 中,而这的设定也致使一个常被新人忽略的问题。

咱们先看问题代码,以下图所示:

  • 一、在 _DemoAppState 中,咱们建立了 DemoPage , 而且把 data 变量赋给了它。
  • 二、DemoPage 在建立 createState 时,又将 data 经过直接传入 _DemoPageState
  • 三、在 _DemoPageState 中直接将传入的 data 经过 Text 显示出来。

运行后咱们一看也没什么问题吧? 可是当咱们点击 4 中的 setState 时,却发现 3 中 Text 没有发现改变, 这是为何呢?

问题就在于前面 StatefulElement 的构建方法和 update 方法:

State 只在 StatefulElement 的构建方法中建立,当咱们调用 setState 触发 update 时,只是执行了 _state.widget = newWidget,而咱们经过 _DemoPageState(this.data) 传入的 data ,在传入后执行setState 时并无改变。

若是咱们采用上图代码中 3 注释的 widget.data 方法,由于 _state.widget = newWidget 时,State 中的 Widget 已经被更新了,Text 天然就被更新了。

三、setState 干了什么?

咱们常说的 setState ,实际上是调用了 markNeedsBuildmarkNeedsBuild 内部会标记 elementdiry,而后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这能够也看出 setState 并非当即生效的。

四、状态共享

前面咱们聊了 Flutter 中 State 的做用和工做原理,接下来咱们看一个老生常谈的对象: InheritedWidget

状态共享是常见的需求,好比用户信息和登录状态等等,而 Flutter 中 InheritedWidget 就是为此而设计的,在第十二篇咱们大体讲过它:

Element 的内部有一个 Map<Type, InheritedElement> _inheritedWidgets; 参数,_inheritedWidgets 通常状况下是空的,只有当父控件是 InheritedWidget 或者自己是 InheritedWidgets 时,它才会有被初始化,而当父控件是 InheritedWidget 时,这个 Map 会被一级一级往下传递与合并。

因此当咱们经过 context 调用 inheritFromWidgetOfExactType 时,就能够经过这个 Map 往上查找,从而找到这个上级的 InheritedWidget

噢,是的,InheritedWidget 共享的是 Widget ,只是这个 Widget 是一个 ProxyWidget ,它本身自己并不绘制什么,但共享这个 Widget 内保存有的值,却达到了共享状态的目的。

以下代码所示,Flutter 内 Theme 的共享,共享的实际上是 _InheritedTheme 这个 Widget ,而咱们经过 Theme.of(context) 拿到的,其实就是保存在这个 Widget 内的 ThemeData

static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
    final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
    if (shadowThemeOnly) {
      /// inheritedTheme 这个 Widget 内的 theme
      /// theme 内有咱们须要的 ThemeData
      return inheritedTheme.theme.data;
    }
    ···
  }
复制代码

这里有个须要注意的点,就是 inheritFromWidgetOfExactType 方法刚了什么?

咱们直接找到 Element 中的 inheritFromWidgetOfExactType 方法实现,以下关键代码所示:

  • 首先从 _inheritedWidgets 中查找是否有该类型的 InheritedElement
  • 查找到后添加到 _dependencies 中,而且经过 updateDependencies 将当前 Element 添加到 InheritedElement_dependents 这个Map 里。
  • 返回 InheritedElement 中的 Widget
@override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    /// 在共享 map _inheritedWidgets 中查找
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      /// 返回找到的 InheritedWidget ,同时添加当前 element 处理
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
   /// 就是将当前 element(this) 添加到  _dependents 里
   /// 也就是 InheritedElement 的 _dependents
   /// _dependents[dependent] = value;
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
复制代码

这里面的关键就是 ancestor.updateDependencies(this, aspect); 这个方法:

咱们都知道,获取 InheritedWidget 通常须要 BuildContext ,如Theme.of(context) ,而 BuildContext 的实现就是 Element因此当咱们调用 context.inheritFromWidgetOfExactType 时,就会将这个 context 所表明的 Element 添加到 InheritedElement_dependents 中。

这表明着什么?

好比当咱们在 StatefulWidget 中调用 Theme.of(context).primaryColor 时,传入的 context 就表明着这个 WidgetElement, 在 InheritedElement 里被“登记”到 _dependents 了。

而当 InheritedWidget 被更新时,以下代码所示,_dependents 中的 Element 会被逐个执行 notifyDependent ,最后触发 markNeedsBuild ,这也是为何当 InheritedWidget 被更新时,经过如 Theme.of(context).primaryColor 引用的地方,也会触发更新的缘由。

下面开始实际分析 Provider

2、Provider

为何会有 Provider

由于 Flutter 与 React 技术栈的类似性,因此在 Flutter 中涌现了诸如flutter_reduxflutter_dvaflutter_mobxfish_flutter 等前端式的状态管理,它们大多比较复杂,并且须要对框架概念有必定理解。

而做为 Flutter 官方推荐的状态管理 scoped_model ,又由于其设计较为简单,有些时候不适用于复杂的场景。

因此在经历了一端坎坷以后,今年 Google I/O 大会以后, Provider 成了 Flutter 官方新推荐的状态管理方式之一。

它的特色就是: 不复杂,好理解,代码量不大的状况下,能够方便组合和控制刷新颗粒度 , 而原 Google 官方仓库的状态管理 flutter-provide 已宣告GG , provider 成了它的替代品。

⚠️注意,`provider` 比 `flutter-provide` 多了个 `r`。

题外话:之前面试时,偶尔会被面试官问到“你的开源项目代码量也很少啊”这样的问题,每次我都会笑而不语,虽然代码量能表明一些成果,可是我是十分反对用代码量来衡量贡献价值,这和你用加班时长来衡量员工价值有什么区别?

0、演示代码

以下代码所示, 实现的是一个点击计数器,其中:

  • _ProviderPageState 中使用MultiProvider 提供了多个 providers 的支持。
  • CountWidget 中经过 Consumer 获取的 counter ,同时更新 _ProviderPageState 中的 AppBarCountWidget 中的 Text 显示。
class _ProviderPageState extends State<ProviderPage> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (_) => ProviderModel()),
      ],
      child: Scaffold(
        appBar: AppBar(
          title: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              var counter =  Provider.of<ProviderModel>(context);
              return new Text("Provider ${counter.count.toString()}");
            },
          )
        ),
        body: CountWidget(),
      ),
    );
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderModel>(builder: (context, counter, _) {
      return new Column(
        children: <Widget>[
          new Expanded(child: new Center(child: new Text(counter.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  counter.add();
                },
                color: Colors.blue,
                child: new Text("+")),
          )
        ],
      );
    });
  }
}

class ProviderModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

因此上述代码中,咱们经过 ChangeNotifierProvider 组合了 ChangeNotifier (ProviderModel) 实现共享;利用了 Provider.ofConsumer 获取共享的 counter 状态;经过调用 ChangeNotifiernotifyListeners(); 触发更新。

这里几个知识点是:

  • 一、 Provider 的内部 DelegateWidget 是一个 StatefulWidget ,因此能够更新且具备生命周期。

  • 二、状态共享是使用了 InheritedProvider 这个 InheritedWidget 实现的。

  • 三、巧妙利用 MultiProviderConsumer 封装,实现了组合与刷新颗粒度控制。

接着咱们逐个分析

一、Delegate

既然是状态管理,那么确定有 StatefulWidgetsetState 调用。

Provider 中,一系列关于 StatefulWidget 的生命周期管理和更新,都是经过各类代理完成的,以下图所示,上面代码中咱们用到的 ChangeNotifierProvider 大体经历了这样的流程:

  • 设置到 ChangeNotifierProviderChangeNotifer 会被执行 addListener 添加监听 listener
  • listener 内会调用 StateDelegateStateSetter 方法,从而调用到 StatefulWidgetsetState
  • 当咱们执行 ChangeNotifernotifyListeners 时,就会最终触发 setState 更新。

而咱们使用过的 MultiProvider 则是容许咱们组合多种 Provider ,以下代码所示,传入的 providers 会倒序排列,最后组合成一个嵌套的 Widget tree ,方便咱们添加多种 Provider

@override
  Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }

  /// Clones the current provider with a new [child].
  /// Note for implementers: all other values, including [Key] must be
  /// preserved.
  @override
  MultiProvider cloneWithChild(Widget child) {
    return MultiProvider(
      key: key,
      providers: providers,
      child: child,
    );
  }
复制代码

经过 Delegate 中回调出来的各类生命周期,如 Disposer,也有利于咱们外部二次处理,减小外部 StatefulWidget 的嵌套使用。

二、InheritedProvider

状态共享确定须要 InheritedWidgetInheritedProvider 就是InheritedWidget 的子类,全部的 Provider 实现都在 build 方法中使用 InheritedProvider 进行嵌套,实现 value 的共享。

三、Consumer

ConsumerProvider 中比较有意思的东西,它自己是一个 StatelessWidget , 只是在 build 中经过 Provider.of<T>(context) 帮你获取到 InheritedWidget 共享的 value

final Widget Function(BuildContext context, T value, Widget child) builder;

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

那咱们直接使用 Provider.of<T>(context) ,不使用 Consumer 能够吗?

固然能够,可是你还记得前面,咱们在介绍 InheritedWidget 时所说的:

传入的 context 表明着这个 WidgetElementInheritedElement 里被“登记”到 _dependents 了。

Consumer 作为一个单独 StatelessWidget它的好处就是 Provider.of<T>(context) 传入的 context 就是 Consumer 它本身。 这样的话,咱们在须要使用 Provider.value 的地方用 Consumer 作嵌套, InheritedWidget 更新的时候,就不会更新到整个页面 , 而是仅更新到 Consumer 这个 StatelessWidget

因此 Consumer 贴心的封装了 contextInheritedWidget 中的“登记逻辑”,从而控制了状态改变时,须要更新的精细度。

同时库内还提供了 Consumer2Consumer6 的组合,感觉下 :

@override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<A>(context),
      Provider.of<B>(context),
      Provider.of<C>(context),
      Provider.of<D>(context),
      Provider.of<E>(context),
      Provider.of<F>(context),
      child,
    );
复制代码

这样的设定,相信用过 BLoC 模式的同窗会感受很贴心,之前正经常使用作 BLoC 时,每一个 StreamBuildersnapShot 只支持一种类型,多个时要不就是多个状态合并到一个实体,要不就须要多个StreamBuilder嵌套。

固然,若是你想直接利用 LayoutBuilder 搭配 Provider.of<T>(context) 也是能够的:

LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              var counter =  Provider.of<ProviderModel>(context);
              return new Text("Provider ${counter.count.toString()}");
            }
复制代码

其余的还有 ValueListenableProviderFutureProviderStreamProvider 等多种 Provider ,可见整个 Provider 的设计上更贴近 Flutter 的原生特性,同时设计也更好理解,而且兼顾了性能等问题。

Provider 的使用指南上,更详细的 Vadaski《Flutter | 状态管理指南篇——Provider》 已经写过,我就不重复写轮子了,感兴趣的能够过去看看。

自此,第十五篇终于结束了!(///▽///)

资源推荐

完整开源项目推荐:

相关文章
相关标签/搜索