48

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

 4 years ago
source link: https://juejin.im/post/5d0634c7f265da1b91639232
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

文章汇总地址:

Flutter 完整实战实战系列文章专栏

Flutter 番外的世界系列文章专栏

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

一、State

1、State 是什么?

我们知道 Flutter 宇宙中万物皆 Widget ,而 Widget@immutable 即不可变的,所以每个 Widget 状态都代表了一帧。

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

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

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

  • Flutter 中的 Widget 在一般情况下,是需要通过 Element 转化为 RenderObject 去实现绘制的。

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

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

1

State 是在哪里被创建的?

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

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

1

我们先看问题代码,如下图所示:

  • 1、在 _DemoAppState 中,我们创建了 DemoPage , 并且把 data 变量赋给了它。
  • 2、DemoPage 在创建 createState 时,又将 data 通过直接传入 _DemoPageState
  • 3、在 _DemoPageState 中直接将传入的 data 通过 Text 显示出来。

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

1

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

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

如果我们采用上图代码中 3 注释的 widget.data 方法,因为 _state.widget = newWidget 时,State 中的 Widget 已经被更新了,Text 自然就被更新了。

3、setState 干了什么?

我们常说的 setState ,其实是调用了 markNeedsBuildmarkNeedsBuild 内部会标记 elementdiry,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这可以也看出 setState 并不是立即生效的。

1

4、状态共享

前面我们聊了 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 引用的地方,也会触发更新的原因。

1

下面开始实际分析 Provider

二、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(); 触发更新。

这里几个知识点是:

  • 1、 Provider 的内部 DelegateWidget 是一个 StatefulWidget ,所以可以更新且具有生命周期。

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

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

接着我们逐个分析

1、Delegate

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

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

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

而我们使用过的 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 的嵌套使用。

2、InheritedProvider

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

3、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》 已经写过,我就不重复写轮子了,感兴趣的可以过去看看。

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

完整开源项目推荐:
1




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK