一,前言
在开发 Flutter 的过程中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。在这篇文章中我们会深入浅出的介绍什么是 Key,以及应该使用 key 的具体场景。
二,什么是Key
在 Flutter 中我们经常与状态打交道。我们知道 Widget 可以有 Stateful 和 Stateless 两种。Key 能够帮助开发者在 Widget tree 中保存状态,在一般的情况下,我们并不需要使用 Key。那么,究竟什么时候应该使用 Key呢。
我们来看看下面这个例子。
class StatelessContainer extends StatelessWidget { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
这是一个很简单的 Stateless Widget,显示在界面上的就是一个 100 * 100 的有颜色的 Container。 RandomColor 能够为这个 Widget 初始化一个随机颜色。
我们现在将这个Widget展示到界面上。
class Screen extends StatefulWidget { @override _ScreenState createState() => _ScreenState(); } class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatelessContainer(), StatelessContainer(), ]; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: widgets, ), ), floatingActionButton: FloatingActionButton( onPressed: switchWidget, child: Icon(Icons.undo), ), ); } switchWidget(){ widgets.insert(0, widgets.removeAt(1)); setState(() {}); } }
这里在屏幕中心展示了两个 StatelessContainer 小部件,当我们点击 floatingActionButton 时,将会执行 switchWidget 并交换它们的顺序。
看上去并没有什么问题,交换操作被正确执行了。现在我们做一点小小的改动,将这个 StatelessContainer 升级为 StatefulContainer。
class StatefulContainer extends StatefulWidget { StatefulContainer({Key key}) : super(key: key); @override _StatefulContainerState createState() => _StatefulContainerState(); } class _StatefulContainerState extends State<StatefulContainer> { final Color color = RandomColor().randomColor(); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, color: color, ); } }
在 StatefulContainer 中,我们将定义 Color 和 build方法都放进了 State 中。
现在我们还是使用刚才一样的布局,只不过把 StatelessContainer 替换成 StatefulContainer,看看会发生什么。
这时,无论我们怎样点击,都再也没有办法交换这两个Container的顺序了,而 switchWidget 确实是被执行了的。
为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey。
class _ScreenState extends State<Screen> { List<Widget> widgets = [ StatefulContainer(key: UniqueKey(),), StatefulContainer(key: UniqueKey(),), ]; ···
然后这两个 Widget 又可以正常被交换顺序了。
看到这里大家肯定心中会有疑问,为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们将涉及 Widget 的 diff 更新机制。
-
Widget 更新机制
下面来来看Widget的源码。@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; ··· static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }
我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。
canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。
-
-
StatelessContainer 比较过程
在 StatelessContainer 中,我们并没有传入 key ,所以只比较它们的 runtimeType。我们将 color 属性定义在了 Widget 中,这将导致他们具有不同的 runtimeType。所以在 StatelessContainer 这个例子中,Flutter能够正确的交换它们的位置。
-
-
-
StatefulContainer 比较过程
而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 false,在 Flutter 看来,并没有发生变化。所以这两个 Element 将不会交换位置。而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 true,现在 Flutter 就可以正确的感知到两个 Widget 交换了顺序了。 (这里 runtimeType 相同,key 不同)
-
-
- 总结:
我们在构建Flutter的UI时是以Widget的形式『拼接』出来的,组件树作为UI每一个组件都对应一个元素(原文中是Slot),从而形成了『元素树』(Element Tree),元素树的内容非常简单,只包含了组件的类型和子元素的引用(Type),你可以把元素树当做Flutter App中的骨架(skeleton),它只展现了App的结构,并不包含其他具体的信息。 当我们交换组件树中的元素时,组件确实进行了交换,但是元素树却不一定。Flutter会先遍历(walk)整个元素树,从Row上的主元素,到主元素的子元素,查看整体的结构是否发生了变化,当然,它检查的只能是元素的Type和Key,在给出的例子中,当我们不设置Key时,元素树对比Type,发现Type并没有发生变化,而Flutter却是用元素树和元素对应的状态(可用或者不可用),来决定这个元素是否应该显示出来,所以在界面中并没有发生改变,但是当我们加入Key之后,对比的对象多了一个,并且是和之前不一样的,Flutter察觉到之后,立即改变了元素的状态,让它变为『无用状态』(deactivate),当遍历完之后,Flutter会浏览(look through)这些不匹配的元素(non-matched children)通过相应的引用为之找到对应的组件。当所有的元素都匹配完成之后,Flutter会刷新界面,展现出我们预想的。
- 总结:
-
比较范围
为了提升性能 Flutter 的比较算法(diff)是有范围的,它并不是对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), Padding( padding: const EdgeInsets.all(8.0), child: StatefulContainer(key: UniqueKey(),), ), ]; ···
在这个例子中,我们将两个带 key 的 StatefulContainer 包裹上 Padding 组件,然后点击交换按钮,会发生下面这件奇妙的事情。
结论:两个 Widget 的 Element 并不是交换顺序,而是被重新创建了。
分析:(1)我们分析一下这次的Widget Tree 和 Element Tree,当我们交换元素后,Flutter element-to-widget matching algorithm,(元素-组件匹配算法),开始进行对比,算法每次只对比一层,即Padding这一层。显然,Padding并没有发生本质的变化。
(2)于是开始进行第二层对比,在对比时Flutter发现元素与组件的Key并不匹配,于是,把它设置成不可用状态,但是这里所使用的Key只是本地Key(Local Key),Flutter并不能找到另一层里面的Key(即另外一个Padding Widget中的Key)所以,Flutter就创建了一个新的Widget,而这个Widget的颜色就成了我们看到的『随机色』。
总结:
所以为了解决这个问题,我们需要将 key 放到 Row 的 children 这一层级。
··· class _ScreenState extends State<Screen> { List<Widget> widgets = [ Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), Padding( key: UniqueKey(), padding: const EdgeInsets.all(8.0), child: StatefulContainer(), ), ]; ···
现在我们又可以愉快的玩耍了(交换 Widget 顺序)了。
三,Key 的种类
-
Key
@immutable abstract class Key { const factory Key(String value) = ValueKey<String>; @protected const Key.empty(); }
默认创建 Key 将会通过工厂方法根据传入的 value 创建一个 ValueKey。
Key 派生出两种不同用途的 Key:LocalKey 和 GlobalKey。
-
Localkey
LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey。
Localkey 派生出了许多子类 key:
-
- ValueKey : ValueKey('String')
- ObjectKey : ObjectKey(Object)
- UniqueKey : UniqueKey()
Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')
-
GlobalKey
@optionalTypeArgs abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ··· static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}; static final Set<Element> _debugIllFatedElements = HashSet<Element>(); static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{}; ··· BuildContext get currentContext ··· Widget get currentWidget ··· T get currentState ···
GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。你可以通过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。
注意:GlobalKey 是非常昂贵的,需要谨慎使用。
四,什么时候需要使用 Key
-
ValueKey
如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。
这时候就需要使用 ValueKey
return TodoItem( key: ValueKey(todo.task), todo: todo, onDismissed: (direction){ _removeTodo(context, todo); }, );
-
ObjectKey
如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。
我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。
这时候你需要使用 ObjectKey!
-
UniqueKey
如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。
不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。(还不如不用?)
-
PageStorageKey
当你有一个滑动列表,你通过某一个 Item 跳转到了一个新的页面,当你返回之前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey 它将能够保持 Sliver 的滚动状态。
-
GlobalKey
GlobalKey 能够跨 Widget 访问状态。 在这里我们有一个 Switcher 小部件,它可以通过 changeState 改变它的状态。class SwitcherScreenState extends State<SwitcherScreen> { bool isActive = false; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Switch.adaptive( value: isActive, onChanged: (bool currentStatus) { isActive = currentStatus; setState(() {}); }), ), ); } changeState() { isActive = !isActive; setState(() {}); } }
但是我们想要在外部改变该状态,这时候就需要使用 GlobalKey。
class _ScreenState extends State<Screen> { final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>(); @override Widget build(BuildContext context) { return Scaffold( body: SwitcherScreen( key: key, ), floatingActionButton: FloatingActionButton(onPressed: () { key.currentState.changeState(); }), ); } }
这里我们通过定义了一个 GlobalKey<SwitcherScreenState> 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。
五,总结:
上面的例子,因为没有数据,所以使用了UniqueKey,在真实的开发中,我们可以用Model中的id作为ObjectKey。
GlobalKey其实是对应于LocalKey,上面我们说Padding中的就是LocalKey,Global即可以在多个页面或者层级复用,比如两个页面也可也同时保持一个状态。