本篇将带你深刻理解 Flutter 中 State 的工做机制,并经过对状态管理框架 Provider 解析加深理解,看完这一篇你将更轻松的理解你的 “State 大后宫” 。前端
前文:git
⚠️第十二篇中更多讲解状态的是管理框架,本篇更多讲解 Flutter 自己的状态设计。 github
咱们知道 Flutter 宇宙中万物皆 Widget
,而 Widget
是 @immutable
即不可变的,因此每一个 Widget
状态都表明了一帧。面试
在这个基础上, StatefulWidget
的 State
帮咱们实现了在 Widget
的跨帧绘制 ,也就是在每次 Widget
重绘的时候,经过 State
从新赋予 Widget
须要的绘制信息。redux
这就涉及 Flutter 中 Widget
的实现原理,在以前的篇章咱们介绍过,这里咱们说两个涉及的概念:bash
Flutter 中的 Widget
在通常状况下,是须要经过 Element
转化为 RenderObject
去实现绘制的。app
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
的数据就得以复用了。less
那 State
是在哪里被建立的?ide
以下图所示,StatefulWidget
的 createState
是在 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
,实际上是调用了 markNeedsBuild
,markNeedsBuild
内部会标记 element
为 diry
,而后在下一帧 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
就表明着这个 Widget
的 Element
, 在 InheritedElement
里被“登记”到 _dependents
了。
而当 InheritedWidget
被更新时,以下代码所示,_dependents
中的 Element
会被逐个执行 notifyDependent
,最后触发 markNeedsBuild
,这也是为何当 InheritedWidget
被更新时,经过如 Theme.of(context).primaryColor
引用的地方,也会触发更新的缘由。
下面开始实际分析 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`。
题外话:之前面试时,偶尔会被面试官问到“你的开源项目代码量也很少啊”这样的问题,每次我都会笑而不语,虽然代码量能表明一些成果,可是我是十分反对用代码量来衡量贡献价值,这和你用加班时长来衡量员工价值有什么区别?
以下代码所示, 实现的是一个点击计数器,其中:
_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();
触发更新。
这里几个知识点是:
一、 Provider 的内部 DelegateWidget
是一个 StatefulWidget
,因此能够更新且具备生命周期。
二、状态共享是使用了 InheritedProvider
这个 InheritedWidget
实现的。
三、巧妙利用 MultiProvider
和 Consumer
封装,实现了组合与刷新颗粒度控制。
接着咱们逐个分析
既然是状态管理,那么确定有 StatefulWidget
和 setState
调用。
在 Provider 中,一系列关于 StatefulWidget
的生命周期管理和更新,都是经过各类代理完成的,以下图所示,上面代码中咱们用到的 ChangeNotifierProvider
大体经历了这样的流程:
ChangeNotifierProvider
的 ChangeNotifer
会被执行 addListener
添加监听 listener
。listener
内会调用 StateDelegate
的 StateSetter
方法,从而调用到 StatefulWidget
的 setState
。ChangeNotifer
的 notifyListeners
时,就会最终触发 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
的嵌套使用。
状态共享确定须要 InheritedWidget
,InheritedProvider
就是InheritedWidget
的子类,全部的 Provider
实现都在 build
方法中使用 InheritedProvider
进行嵌套,实现 value
的共享。
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》 已经写过,我就不重复写轮子了,感兴趣的能够过去看看。
自此,第十五篇终于结束了!(///▽///)