When to Use Keys
Youtube 地址:https://youtu.be/kn0EOS-ZiIc
先看一下 Widget 的构造函数:
const Widget({ this.key });
在之前讲的 StatelessWidget 和 StatefulWidget 的构造函数中,也有同样的东西:
const StatelessWidget({ Key key }) : super(key: key);
const StatefulWidget({ Key key }) : super(key: key);
都有一个 Key 属性,那么这个玩意儿就是有没有用,究竟有什么用,究竟怎么用?今天就来研究下这几个问题。
Key 有用吗?
这不废话么,谁没用整那么个玩意儿在那呢?但似乎之前的例子中,不写 Key 也没啥问题啊,那么看一下下面的代码:
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(MaterialApp(
home: HomeWidget(),
));
}
class HomeWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomeWidgetState();
}
}
class HomeWidgetState extends State<HomeWidget> {
List<Widget> widgetList = [
ColorStatelessWidget(),
ColorStatelessWidget(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Key Test"),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center, children: widgetList),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.find_replace),
onPressed: () {
setState(() {
widgetList.insert(1, widgetList.removeAt(0));
});
},
),
);
}
}
class ColorStatelessWidget extends StatelessWidget {
final Color color = UniqueColorFactory.getColor();
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(5),
width: 100,
height: 100,
color: color,
);
}
}
class UniqueColorFactory {
static Color getColor() {
return Color.fromARGB(255, Random().nextInt(256) + 0,
Random().nextInt(256) + 0, Random().nextInt(256) + 0);
}
}
效果如下:
点击按钮,色块位置交换,在上面的例子中,色块是 StatelessWidget
,我们换成 StatefulWidget
试试:
class HomeWidgetState extends State<HomeWidget> {
List<Widget> widgetList = [
ColorStatefulWidget(),
ColorStatefulWidget(),
];
...
}
class ColorStatefulWidget extends StatefulWidget {
@override
State<ColorStatefulWidget> createState() {
return ColorStatefulWidgetState();
}
}
class ColorStatefulWidgetState extends State<ColorStatefulWidget>{
final Color color = UniqueColorFactory.getColor();
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(5),
width: 100,
height: 100,
color: color,
);
}
}
这次点击按钮,色块位置没发生变化。
我们给 ColorStatefulWidget
加上 key 试试:
class HomeWidgetState extends State<HomeWidget> {
List<Widget> widgetList = [
ColorStatefulWidget(key: UniqueKey(),),
ColorStatefulWidget(key: UniqueKey(),),
];
...
}
class ColorStatefulWidget extends StatefulWidget {
ColorStatefulWidget({Key key}):super(key:key);
...
}
发现替换成功了。
那么,为什么 StatelessWidget 可以直接替换,而 StatefulWidget 却不行呢?为什么 StatefulWidget 加上 key 就可以替换成功呢?
那我们就来深究一下其中到底发生了什么。这一切得显示 Flutter 中 UI 更新机制讲起。
Flutter 中 UI 更新机制
Widget、Element 和 Render 三棵树
在之前的文章中写过,Widget 实际上是数据结构的配置,它本身是没有状态的,开发者编写一个相互嵌套的 Widget 组成一个 Widget 树:
然后他们调用内部的 createElement
,最终生成一个 Element 树。一个 Widget 可以多次复用,生成多个 Element,就像在上面的例子中,ColorWidget 生成了两个 Element。
Widget 是不可变的(所以它的属性应均为 final,这点在之前的例子中并没有刻意添加修饰,IDE 有提示,但我给忽略了),它如要更新便需要重建,如果想要把可变状态与 Widget 关联起来,可以使用 StatefulWidget,StatefulWidget 通过使用 StatefulWidget.createState
方法创建 State
对象,并将之扩充到 Element
以及合并到树中:
Element 最终会调用 createRenderObject
形成一个 RenderObject 树,由 RenderObject 将内容渲染到屏幕上。
当我们需要更新数据时,就得需要重新创建 Widget,Widget 树发生变化,那么 Element 树和 RenderObject 树也随之整个重建吗?
当然不会,Widget 只是一个配置数据结构,创建是非常轻量的,加上 Flutter 团队对 Widget 的创建/销毁做了优化,不用担心整个 Widget 树重新创建所带来的性能问题,但是 Renderobject 就不一样了,Renderobject 涉及到 layout、paint 等复杂操作,是一个真正渲染的 View,整个 View 树重新创建开销就比较大,所以答案是否定的。
Element
用于管理应用 UI 的更新和更改,管理部件的生命周期,每个 Element
都包含对 Widget
和 RenderObject
的引用。
Key 有什么用
上面啰嗦了一大堆,再回到最开始的例子。 我们知道 Element 树中的每个 Element 都包含了对应 Widget 的引用,当 Widget 发生变化时,系统会从根部开始遍历整个 Element 树,以检查新的 Widget 和 Element 树中的 Widget 引用是否相同,如果相同,则使用新的 Widget (生成的 Element)替换树中已存在的,如果不同,则将树中旧的 Element 删除,将新(生成)的加入。
怎么判断是否相同呢?
- 类型是否相同(runtimeType)
- key是否相同(如果设置了的话)
所以对于第一个例子(使用 StatelessWidget)来说,当 Widget 树发生变化,Element 树遍历之后发现新的 Widget 和已存在的 Widget 的 runtimeType 相同(因为我们没有设置 key,所以只检查 runtimeType),所以会替换 Element 树中的 Widget 引用,最终将新的 RenderObject 渲染到屏幕上,完成色块交换目的。
当我们使用 StatefulWidget 时,系统依旧会遍历检查,发现 runtimeType 还是一样,还是进行替换,最终就导致 Flutter 认为这两个控件都没有发生改变。Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变,显示的内容也就不会改变。
当我们给 StatefulWidget 加上 key,系统遍历 Element 树之后,会对比 runtimeType 和 key,这个时候当 Widget 发生变化,系统对比之后会对 Element 进行重建,知道它树中对 Widget 的引用和 Widget 树对应上,这个时候色块自然也就交换位置了。
再重新修改一下之前的例子,给 List 中的元素添加一层 Padding:
List<Widget> widgetList = [
Padding(
padding: EdgeInsets.all(8.0),
child: ColorStatefulWidget(
key: UniqueKey(),
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: ColorStatefulWidget(
key: UniqueKey(),
),
)
];
执行之后会发现,奇怪的事情发生了,两个色块并不是交换顺序,而是重新变换了颜色:
为什么会这样呢?
是因为 Flutter 检查 Widget 和 Element 的对应关系时,每次只检查树的一个层级,什么意思呢?在上面的例子中:
系统先检查 Padding 这一层,发现是匹配的
接着在进入到 Padding 这层中,检查其子控件,发现 key 是不同的,则将 Element 和 Widget 之间的引用删除
系统会在同一层中搜索,是否有相同 key 的 Widget,来替换 Element 中的元素,搜索之后发现没有,所以它会创建一个新 Element 并初始化一个新 State。 就是这个原因,造成色块颜色发生随机改变,每次交换相当于生成了两个新的 Widget。
只在同一层中搜索,是因为我们使用的 key 是 UniqueKey,它是一个 LocalKey,这就会造成当 Widget 与 Element 匹配时,Flutter 只在树中特定级别内查找匹配的 Key。
有没有解决办法呢?
当然有了,把 key 设置给外部的 Padding 就可以了。
Key 的使用位置
从上面的例子中,就可以看出来,我们尽量在当前 Widget 树的顶级 Widget 中设置 key。
Key 的分类
Flutter 中的 Key 主要有两大类:
- LocalKey
- GlobalKey
LocalKey
LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的 Widget 进行比较的情况,也就是上述例子中,有一个多子 Widget 中需要对它的子 widget 进行移动处理,这时候你应该使用Localkey。
Localkey 派生出了许多子类 key:
- ValueKey : ValueKey('String')
- ObjectKey : ObjectKey(Object)
- UniqueKey : UniqueKey()
Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')
GlobalKey
GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。
你可以通过 GlobalKey 找到持有该 GlobalKey 的 Widget,State 和 Element。
所以在 Padding 那例子中,除了将 key 设置给 Padding,还可以直接给子 Widget 设置一个 GlobalKey,这样一来,查找 key 的时,就不是只在同一层级中查找了。
GlobalKey 是非常耗费资源的,需要谨慎使用。
Key 的使用场景
Key
的目的在于为每个 Widget 指明一个唯一的身份,使用何种 Key
就要依具体的使用场景决定。
- ValueKey
例如在一个 ToDo
列表应用中,每个 Todo
Item 的文本是恒定且唯一的。这种情况,适合使用 ValueKey
,value 是文本。
- ObjectKey
假设,每个子 Widget 都存储了一个更复杂的数据组合,比如一个用户信息的地址簿应用。任何单个字段(如名字或生日)可能与另一个条目相同,但每个数据组合是唯一的。在这种情况下, ObjectKey
最合适。
- UniqueKey
如果集合中有多个具有相同值的 Widget,或者如果您想确保每个 Widget 与其他 Widget 不同,则可以使用 UniqueKey
。 在我们的例子中就使用了 UniqueKey
,因为我们没有将任何其他常量数据存储在我们的色块上,并且在构建 Widget 之前我们不知道颜色是什么。
不要在 Key
中使用随机数,如果你那样设置,那么当每次构建 Widget 时,都会生成一个新的随机数,Element 树将不会和 Widget 树做一致的更新。
GlobalKeys
Global Keys有两种用途。
它们允许 Widget 在应用中的任何位置更改父级而不会丢失 State ,或者可以使用它们在 Widget 树 的完全不同的部分中访问有关另一个 Widget 的信息。
比如: 要在两个不同的屏幕上显示相同的 Widget,同时保持相同的 State,则需要使用 GlobalKeys。
在第二种情况下,您可能希望验证密码,但不希望与树中的其他 Widget 共享该状态信息,可以使用
GlobalKey
持有一个表单Form
的State
。 Flutter.dev 上有这个例子 Building a form with validation。
其实 GlobalKeys 看起来有点像全局变量。有也其他更好的方法达到 GlobalKeys 的作用,比如 InheritedWidget、Redux 或 Block Pattern。