0
点赞
收藏
分享

微信扫一扫

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

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

文章汇总地址:


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

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


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

一、State

1、State 是什么?

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

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

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

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

  • Flutter 中的 ​​Widget​​ 在一般情况下,是需要通过 ​​Element​​ 转化为 ​​RenderObject​​ 去实现绘制的。
  • ​Element​​ 是 ​​BuildContext​​ 的实现类,同时 ​​Element​​ 持有 ​​RenderObject​​ 和 ​​Widget​​ ,我们代码中的 Widget build(BuildContext context) {} 方法,就是被 ​Element​ 调用的。

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

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

State 是在哪里被创建的?

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

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

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

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

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

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

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

问题就在于前面 ​​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​​​ ,其实是调用了 ​​markNeedsBuild​​​ ,​markNeedsBuild 内部会标记 ​element​​diry​,然后在下一帧 ​WidgetsBinding.drawFrame​ 才会被绘制,这可以也看出 ​setState​ 并不是立即生效的。

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

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 就代表着这个 ​Widget​​Element​, 在 ​InheritedElement​ 里被“登记”到 ​_dependents​ 了。

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

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


下面开始实际分析 Provider


二、Provider

为什么会有 Provider

因为 Flutter 与 React 技术栈的相似性,所以在 Flutter 中涌现了诸如​​flutter_redux​​​ 、​​flutter_dva​​​ 、 ​​flutter_mobx​​​ 、 ​​fish_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​​​ 中的​​AppBar​​​ 和​​CountWidget​​​ 中的​​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.of​​​ 和 ​​Consumer​​​ 获取共享的 ​​counter​​​ 状态;通过调用 ​​ChangeNotifier​​​ 的 ​​notifyListeners();​​ 触发更新。

这里几个知识点是:

  • 1、 Provider 的内部 ​​DelegateWidget​​ 是一个 ​​StatefulWidget​​ ,所以可以更新且具有生命周期。
  • 2、状态共享是使用了 ​​InheritedProvider​​ 这个 ​​InheritedWidget​​ 实现的。
  • 3、巧妙利用 ​​MultiProvider​​ 和 ​​Consumer​​ 封装,实现了组合与刷新颗粒度控制。

接着我们逐个分析

1、Delegate

既然是状态管理,那么肯定有 ​​StatefulWidget​​​ 和 ​​setState​​ 调用。

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

  • 设置到​​ChangeNotifierProvider​​​ 的​​ChangeNotifer​​​ 会被执行​​addListener​​​ 添加监听​​listener​​。
  • ​listener​​​ 内会调用​​StateDelegate​​​ 的​​StateSetter​​​ 方法,从而调用到​​StatefulWidget​​​ 的​​setState​​。
  • 当我们执行​​ChangeNotifer​​​ 的​​notifyListeners​​​ 时,就会最终触发​​setState​​ 更新。

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

而我们使用过的 ​​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

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

3、Consumer

​Consumer​​​ 是 ​​Provider​​​ 中比较有意思的东西,它本身是一个 ​​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​​​ 代表着这个 ​​Widget​​​ 的 ​​Element​​​ 在 ​​InheritedElement​​​ 里被“登记”到 ​​_dependents​​ 了。


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

所以 Consumer 贴心的封装了 ​context​​InheritedWidget​ 中的“登记逻辑”,从而控制了状态改变时,需要更新的精细度。

同时库内还提供了 ​​Consumer2​​​ ~ ​​Consumer6​​ 的组合,感受下 :

@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 时,每个 ​​StreamBuilder​​​ 的 ​​snapShot​​ 只支持一种类型,多个时要不就是多个状态合并到一个实体,要不就需要多个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()}");
}

其他的还有 ​​ValueListenableProvider​​​ 、​​FutureProvider​​​ 、​​StreamProvider​​​ 等多种 ​​Provider​​ ,可见整个 Provider 的设计上更贴近 Flutter 的原生特性,同时设计也更好理解,并且兼顾了性能等问题。

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


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


资源推荐

  • 本文Demo :​​github.com/CarGuo/stat…​​
  • Github :​​github.com/CarGuo/​​
  • 开源 Flutter 完整项目:​​github.com/CarGuo/GSYG…​​
  • 开源 Flutter 多案例学习型项目: ​​github.com/CarGuo/GSYF…​​
  • 开源 Fluttre 实战电子书项目:​​github.com/CarGuo/GSYF…​​
完整开源项目推荐:
  • GSY Flutter 实战系列电子书
  • GSYGithubApp Flutter
  • GSYGithubApp React Native
  • GSYGithubAppWeex

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

举报

相关推荐

0 条评论