⚠️第十二篇中更多讲解状态的是管理框架,本篇更多讲解 Flutter 本身的状态设计。
一、State1、State 是什么?我们知道 Flutter 宇宙中万物皆 Widget ,而 Widget 是 @immutable 即不可变的,所以每个 Widget 状态都代表了一帧。 在这个基础上, StatefulWidget 的 State 帮我们实现了在 Widget 的跨帧绘制 ,也就是在每次 Widget 重绘的时候,通过 State 重新赋予 Widget 需要的绘制信息。 2、State 怎么实现跨帧共享?这就涉及 Flutter 中 Widget 的实现原理,在之前的篇章我们介绍过,这里我们说两个涉及的概念: 了解这个两个概念后,我们先看下图,在 Flutter 中构建一个 Widget ,首先会创建出这个 Widget 的 Element ,而事实上 State 实现跨帧共享,就是将 State 保存在Element 中,这样 Element 每次调用 Widget build() 时,是通过 state.build(this); 得到的新 Widget ,所以写在 State 的数据就得以复用了。 那 State 是在哪里被创建的? 如下图所示,StatefulWidget 的 createState 是在 StatefulElement 的构建方法里创建的, 这就保证了只要 Element 不被重新创建,State 就一直被复用。 同时我们看 update 方法,当新的 StatefulWidget 被创建用于更新 UI 时,新的 widget 就会被重新赋予到 _state 中,而这的设定也导致一个常被新人忽略的问题。 我们先看问题代码,如下图所示: 1、在 _DemoAppState 中,我们创建了 DemoPage , 并且把 data 变量赋给了它。 2、DemoPage 在创建 createState 时,又将 data 通过直接传入 _DemoPageState 。 3、在 _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 自然就被更新了。 3、setState 干了什么?我们常说的 setState ,其实是调用了 markNeedsBuild ,markNeedsBuild 内部会标记 element 为 diry ,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这可以也看出 setState 并不是立即生效的。 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 引用的地方,也会触发更新的原因。 下面开始实际分析 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、演示代码如下代码所示, 实现的是一个点击计数器,其中: 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 更新。
而我们使用过的 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、ConsumerConsumer 是 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 的原生特性,同时设计也更好理解,并且兼顾了性能等问题。
|