Flutter 的设计思想是将视图与数据分离,思路颇似 React。在一个 Flutter App 中,当框架需要重新渲染视图时,会调用 dirty widget 的 build 方法,该方法则会使用外部的数据和状态构建 Widget Tree,进而使框架在内部构建出 Render Tree。后者我们在日常开发中基本不需要接触,所以主要的工作就是构建 Widget Tree 的逻辑。
Flutter 的 widget 分为两种,一种是 StatelessWidget,另一种则是 StatefulWidget,前者一般用于静态内容的显示,而后者则用于存在交互和逻辑的内容。
本文我们主要讨论一下 StatefulWidget。
首先看一个例子:
@override
Widget build(BuildContext context) {
var contents = <Widget>[
new RaisedButton(
child: const Text('Change Color'),
onPressed: () { _invertColorFlag(); },
),
const SizedBox(height: 10.0),
new RaisedButton(
child: const Text('Change Position'),
onPressed: () { _invertPositionFlag(); },
),
];
final statefulWidget = new _MyStatefulWidget(
value: 10,
textColor: _colorFlag ? const Color(0xffff0000) : null,
);
if (_positionFlag) {
contents.add(statefulWidget);
} else {
contents.insert(0, statefulWidget);
}
return new Scaffold(
appBar: new AppBar(
title: const Text('Stateful Demo'),
),
body: new Center(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: contents,
),
),
);
}
_MyStatefulWidget 是一个自定义的 widget,用于表现一个简单的计数器,并且我们可以通过参数修改它的初始值和样式:

实例 app 的运行效果如下(知乎的上传图片太垃圾了,直接报 Memory Limit Exceeded,这里就传静态图片了):


但是当我们点击 “Change Position” 按钮时就出现问题了:

计数器的状态被重置了,为什么会出现这种情况,这就要从 Flutter 的工作原理说起了。
当我们通过 setState 方法变更一个 widget 的状态时,Flutter 会重新调用其对应 State 的 build 方法来重新构造 Widget Tree,Flutter 获取到最新的 Widget Tree 后会与当前的 Widget Tree 做对比,是不是非常像 VDom 的思想?当每个 Widget 在树中的位置一致时(只修改了 Widget 的属性),Flutter 将识别到,并会复用之前的 State 对象,因此这个 Widget 的状态就会得以保留。
然而当我们点击 “Change Position” 按钮时,我们交换了计数器在 Widget Tree 中的位置,这使得内部的 tree differ 无法确定新出现的计数器是不是原来的那个计数器,所以 Flutter 就会决定不复用它的 State,并通过 createState 来重新创建一个新的 State。
其实解决办法很简单,用过 React 的同学已经知道答案了,那就是使用 key。
通过 key 可以唯一标识一个 Widget,key 在列表类视图中尤为重要,因为它会让 differ 更高效地计算出哪些元素是新添加的,哪些元素是被移除或交换位置的,Flutter 也是如此。在 Flutter 中创建一个 key 很简单,只需要一行:
final _statefulKey = new GlobalKey(debugLabel: '_MyStatefulWidget');
这个 key 比较特殊,因为它是整个 app 中唯一的,Flutter 负责创建它,并保证它的唯一性。这种 key 在我们这个例子中比较适用,但是在列表类视图中就不那么适用了,我们希望视图与数据一一对应,这时候就需要 ValueKey 了。
Mix-and-match 模式
Flutter 官方提倡使用一种名为 mix-and-match 的模式来设计有状态的 Widget,这也很类似于 React 中的 Lifting State Up 最佳实践。简单来说就是不让输入类 Widget(如 Slider、Checkbox 等,也包括我们上面的计数器)自己管理“数据状态”,所谓数据状态就是 Widget 所展示的主要数据,比如 Slider 最主要是去图形化地表示一个 fraction,这个 fraction 数值应该由父视图去管理,Slider 自身只需要管理一些动画、视觉方面的状态。
References: