Flutter Key
当widget
在widget tree
中移动时,key
可以保留它的状态.
key
可用于保留用户的滚动位置,或在修改集合时保持状态.
什么时候需要Key
如果你还没有用过key
,说明你可能还不需要使用key
.
事实也正是如此,大多数时候,我们不需要使用key
.
但是如果你发现自己需要添加,删除或重新排序处于某种状态的相同类型的widget
的集合的时候,可能就是需要用到key
的时候了.
我们先用一个官方的例子来看一下key
的作用 , 交换两个只有颜色不同的widget
class KeyStudy extends StatefulWidget {
@override
_KeyStudyState createState() => _KeyStudyState();
}
class _KeyStudyState extends State<KeyStudy> {
List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [StatelessColorfulWidget(), StatelessColorfulWidget()];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: tiles,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: () {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}),
);
}
}
class StatelessColorfulWidget extends StatelessWidget {
static List<Color> colorList = [Colors.blue, Colors.yellow, Colors.red];
final defaultColor = colorList[new Random().nextInt(3)];
@override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: defaultColor,
);
}
}
代码不复杂,我们先定义了一个无状态的widget
:StatelessColorfulWidget
,
然后在页面上并列放置2个该widget
.
通过点击按钮来调换这两个widget
的位置.
看起来很正常 , 操作也符合我们的预期 , 此时我们将StatelessColorfulWidget
从StatelessWidget
换成StatefulWidget
,并将颜色存储在state
中 ,
class StatefulColorfulWidget extends StatefulWidget {
StatefulColorfulWidget({Key key}) : super(key: key);
@override
_StatefulColorfulWidgetState createState() => _StatefulColorfulWidgetState();
}
class _StatefulColorfulWidgetState extends State<StatefulColorfulWidget> {
Color defaultColor;
@override
void initState() {
super.initState();
defaultColor = StatelessColorfulWidget.colorList[new Random().nextInt(3)];
}
@override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: defaultColor,
);
}
}
代码依旧不复杂 , 然后用StatefulColorfulWidget
替代tiles
中的StatelessColorfulWidget
,来看一下效果:
我点了几下按钮,一点反应都没有,但是此时如果加个key呢?
像这样:
@override
void initState() {
super.initState();
tiles = [
StatefulColorfulWidget(key: UniqueKey()),
StatefulColorfulWidget(key: UniqueKey())
];
}
之后我们看看效果:
可以看到,加了key
之后,又能正常交换了.
注意 : 如果你在写以上代码来看效果的时候,记得每次修改重新运行app
,因为我们的tiles
是定义在initState()
中的,
热重载不会执行initState()
方法 , 所以可能效果与预期不符.
如果集合中的整个widget
子树是无状态的, 则不需要key
.
说到无状态,就想起了cool
, 确实, 这个key
确实很cool
.
为什么会这样
在无状态的widget
中, Row
为其子widget提供了一组有序插槽.
对于每一个widget
,Flutter都会构建一个相应的Element
.
Element tree
只保存有关每个widget
的类型,以及对子元素的引用信息,
我们可以将 Element tree
看做是Flutter应用程序的骨架,它显示了应用程序的结构, 但可以用过引用原始widget
来查找所有其他信息.
当我们交换行中widget
的顺序时,flutter会遍历Element Tree
来查看骨架的结构是否相同.
从父元素,也就是这里的Row
开始,然后检查它的子元素,Element Tree
会检查新的widget
和旧的widget
的runTimeType
和key
是否相同.
如果是,就会更新对新的widget
的引用, 在第一种情况下,我们使用的是StatelessWidget
,因为这个widget
没有key
,所以flutter只检查它的类型.
此时,我们再对StatefulWidget
的情况进行同样的分析:
基本与之前一样,有同样的Widget Tree
和Element Tree
,但是现在也有一对state
对象,并将颜色的信息存储到了这里.
而不是像StatelessWidget
一样存储在Widget
自身中.
所以此时当我们交换2个wdiget
的顺序时,Flutter遍历Element Tree
,检查widget
的类型和key
,并更新引用.
Flutter使用Element Tree
及其相应的状态来确定实际显示的内容 , 所以看起来widget
没有正确交换.
而当我们使用了key
之后,Flutter检查会发现key
不匹配 , 所以Flutter 会 deactive这些element, 并查找与这个key
相同的element
,找到后会更新其对widget
的引用.
此处,交换之后,对第一个StatefulTileWidget
进行检查的时候,发现右侧对应的StatefulileElement
的key
不匹配,会为它寻找匹配的element
第二个也是如此 , 现在就可以正常更换widget
了.
因为如果要修改集合中的widget
的数量或者顺序,那么key
是很有用的.
此处我们举例仅使用了颜色,但实际上我们在state
中会存储更加复杂的东西, 比如播放动画,显示用户输入的数据,滚动位置等等,这些都涉及到状态.
所以,现在我们有一个场景,需要使用key
,这个key
应该放在哪里呢?
将Key放在Widget树的哪个位置
我们应该在widget
树的顶部指定一个需要保留的key
.
至于为神马是在顶部,我们通过下面一个例子来看一下原因:
我很偷懒的用快捷方式为这两个StatefulColorfulWidget
添加了Padding
,然后我们看一下效果:
看起来好像不是交换啊?点击按钮时,widget
变成了随机的颜色,为什么只是将widget
用Padding
包裹之后就变成了这样呢?
(有人可能会发现widget的大小改变了,那是因为之前的widget宽度是200,加了padding之后超出屏幕范围了,我偷偷改小了)
下图是添加了Padding
之后的Widget Tree
和Element Tree
的样子
当我们交换子项的位置时,Flutter的element
到widget
的匹配算法是一次查看树的一个级别.
在第一级子项,一起都匹配正确,
但是在第二级时,Flutter会注意到element
和widget
的key
不匹配,因此会停用这个element
,去掉它们之间的关联,
因为我们此处使用的UniqueKey
是LocalKey
,这意味着当将widget
和element
匹配时,Flutter仅查找在树中特定级别内相匹配的key
.
因为无法在该级别找到具有相同key
的element
,所以会创建一个新的,并初始化一个新的状态 ,
在这种情况下,widget
会有一个新的颜色.
当然,另外一个也会出现同样的问题.
那如果我们在Padding
添加key
呢?Flutter就会找到正确的key
来重新建立连接,就像前面的示例一样.
所以,我们现在知道什么时候使用key
,以及将key
放在哪里了.
但是如果你去Flutter的文档里查看key
,会发现有几种不同的key
,那么我们该使用哪一个呢?
应该使用哪个Key
Key的种类
首先我们肯定要知道的是,key究竟有哪几种?
GlobalKey有两种用途,它允许widget
在App的任何位置更改父级而不会丢失状态,或者可以使用它们在Widget Tree
完全不同的部分中访问有关另一个widget
的信息.
比如要在两个不同的屏幕显示相同的widget
,并保持所有相同的状态, 此时可以使用GlobalKey.
而LocalKey
又有以下几个子类:
- ObjectKey : 从对象中获取其标识的键用作其值。
- UniqueKey : 仅等于自身的Key。
- ValueKey : 使用特定类型的值标识自己的键。
- PageStorageKey : ValueKey的子类, 它定义了PageStorage值的保存位置。
PageStorageKey是用来存储用户滚动位置的专用key,因此APP可以保留它以供后面使用.
我们要注意3点:
1.在具有相同父元素的Elements中,键必须唯一。相比之下,GlobalKeys在整个应用程序中必须唯一。
2.Key的子类应该是LocalKey或GlobalKey的子类.
3.GlobalKey更为昂贵,因此如果没有必要,请使用ValueKey, ObjectKey, 或者 UniqueKey.
使用场景
在代办事项列表应用程序中 , 我们可能希望待办事项的文本是恒定且独特的,这种情况,ValueKey
是一个很好的选择,因为它的文本是value
.
如果每个Widget
有更复杂的数据组合,任何单个字段都有可能与另一个条目相同,但组合起来是唯一的,这时可以用ObjectKey
.
如果集合中有多个具有相同值的widget
,或者我们想确保每个widget
与其他的widget
不同,则可以使用UniqueKey
.
我们的示例就是用的UniqueKey
,因为我们没有任何其他常量数据存储在我们的组件上,而且在构建widget
之前,我们还不知道颜色是什么.
小结
当你想要跨widget
树保留状态时 , 应该使用key
.
当修改相同类型的widget
集合时, 要将key
放在要保留的widget
的树的顶部.
并且要根据widget中存储的数据类型选择对应的key
.