Flutter中的Key

本文内容主要翻译自Keys in Flutter, 最初翻译动机是原作者写的比较通俗,其次 key 知识点在 Flutter 中比较重要,但在翻译过程中发现不配合相关源码很难理解作者意思而且看完容易忘,所以加了些注释和理解(详见引述),有什么不对的地方欢迎各位大佬交流指正,多谢!


在使用 Flutter 时,我们经常会遇到一个叫做 Key 的东西。Key 是 Flutter 中几乎所有 widget 都具有的属性。但它并不常用而容易被忽视。为什么 widget 具有 Key 呢?它对我们有什么意义呢?让我们找出答案。

什么是 Key

Flutter 将 Key 描述为 Widget、Element 和 SemanticNodes 的标识符。这是什么意思呢?这意味着 Key 是分配给 Widget 的唯一标识,通过 key 可以与其他 Widget 区分开来。对于 Widget 在 Widget 树中改变位置的情况,Key 帮助保留它们的状态。说明 Key 大多数情况下对于有状态的 Widget 而言更有用,而对于无状态的 Widget 则不太需要。

何时使用 Key

Key 可以放在代码的几乎任何地方而不会造成什么问题。但在不需要的情况下放 Key 只会浪费内存空间。因此,需要了解它的应用场景。

大部分情况下不需要使用 Key。在添加、删除或重排同一类型的 widget 集合时,Key 非常有用。这些 widget 保持某些状态,并且在 widget 树中处于相同的级别。如果没有 Key,更新这样的 widget 集合可能不会产生预期的结果。我们倾向于在像 ListView 或 Stateful widget 的子级上使用 Key,因为其数据会不断变化。

为了进一步说明修改 widget 集合时为什么需要 key,这里用一个简单的示例说明。示例显示了两个颜色块单击按钮时它们可以交换位置。

Record_2023-04-06-16-02-30.gif

该示例有两种实现方式

第一种实现:色块 widget 是无状态的,色值保存在 widget 本身中。当点击 FloatingActionButton,色块会像预期正确地交换位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'package:flutter/material.dart';
import 'package:keys_example/value_key_example.dart';
import 'package:random_color/random_color.dart';

void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:const PositionTiles(),
);
}
}
class PositionTiles extends StatefulWidget {
const PositionTiles({Key? key}) : super(key: key);
@override
State<PositionTiles> createState() => _PositionTilesState();
}
class _PositionTilesState extends State<PositionTiles> {
List<Widget> tiles = [];
@override
void initState() {
super.initState();
tiles = [
StatelessColorTiles(),
StatelessColorTiles(),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Row(mainAxisAlignment:MainAxisAlignment.center,children: tiles,),),
floatingActionButton: FloatingActionButton(child: Icon(Icons.sentiment_very_satisfied, ),onPressed: swapTiles,),
);
}
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}

///色块widget是无状态的
class StatelessColorTiles extends StatelessWidget {
//色值保存在本身控件中
Color myColor = RandomColor().randomColor();
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: myColor,
);
}
}

第二种实现:色块 widget 是有状态的,并将色值保存在状态中。这一次,当点击 FloatingActionButton 时似乎什么都没有发生。为了正确交换平铺位置,我们需要向有状态的 widget 添加 key 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import 'package:flutter/material.dart';
import 'package:keys_example/value_key_example.dart';
import 'package:random_color/random_color.dart';

void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:const PositionTiles(),
);
}
}
class PositionTiles extends StatefulWidget {
const PositionTiles({Key? key}) : super(key: key);
@override
State<PositionTiles> createState() => _PositionTilesState();
}
class _PositionTilesState extends State<PositionTiles> {
List<Widget> tiles = [];
@override
void initState() {
super.initState();
tiles = [
//添加了key参数,若不添加则点击按钮色块不会交互
StatefulColorTiles(key: UniqueKey(),),
StatefulColorTiles(key: UniqueKey(),),

];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Row(mainAxisAlignment:MainAxisAlignment.center,children: tiles,),),
floatingActionButton: FloatingActionButton(child: Icon(Icons.sentiment_very_satisfied, ),onPressed: swapTiles,),
);
}
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorTiles extends StatefulWidget {
const StatefulColorTiles({Key? key}) : super(key: key);
@override
State<StatefulColorTiles> createState() => _StatefulColorTilesState();
}
class _StatefulColorTilesState extends State<StatefulColorTiles> {
///色值保存在state中
late Color myColor;
@override
void initState() {
super.initState();
RandomColor _randomColor = RandomColor();
myColor = _randomColor.randomColor();
}
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: myColor,
);
}
}

总结:该示例表明,仅当 widget 有状态时,才需要设置 key。如果是无状态的 widget 则不需要设置 key。

背后原理

刚刚第二种实现中,使用 key 的代码中实现预期的行为。为什么 key 可以做到这一点呢?让我们来找出答案。

当渲染 widget 时,Flutter 不仅会构建 widget 树,同时也会构建其对应的元素树。元素树持有 widget 树中 widget 的信息及其子 widget 的引用。在修改和重新渲染的过程中,Flutter 查找元素树以查看其是否已改变,以便在元素未改变时可以复用旧元素。


批注及说明:

① widget 树相当于配置,元素树相当于实例对象。widget 相当于 json,元素树相当于 json 解析后的 bean。

② 关于改变的判断条件 : widget 类型 和 key 值 ,若在没用 key 的情况下,若类型相同则表示新旧 widget 可复用

1
2
3
4
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}

在本例中,原色块是 A 和 B, 交互后 oldWidget = A newWidget = B 因为 A 和 B 是同类型 StatelessColorTiles ,则表示 A 在原来在元素树中的 E(A)元素在交换后是可以继续供 B 复用。


前置知识

W(A)和 W(B)交换后调用 setState 框架发生了啥?

将自身元素对象标记为脏元素并放到脏元素数组中,期间会触发 Vsync 信号,等待系统更新脏元素数组中的元素。

整个过程会递归执行,因为 build 方法中是嵌套关系,会一层层遍历来执行如下过程,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
//省略步骤....
if (child != null) {
// 两个widget相同,位置不同更新位置,返回child。这里比较的是hashCode
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
// 我们的交换例子处理在这里
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// 如果无法更新复用,那么创建一个新的Element并返回。
return inflateWidget(newWidget, newSlot);
}

无状态的示例中,每个色块 widget 都有其对应的色块元素。因为色值属性保存在 widget 自身中,当交换色块 widget 时,元素树上的引用没变依然是原来色块元素。因此,正确交互实现预期行为。

很显然,如果 W(A) 与 W(B)在交互后,canUpdate 返回 true。此时执行 child.update(newWidget),其中
child = Element(A)
child.widget = W(A)
newWidget = W(B)
即执行:Elelement(A).update(W(B))

因为 W(B) 中保存了 B 的色值,色值随身携带原因,所有达到预期行为,W(A)与 W(B)在交互后 setState 交互了颜色。

有状态的示例中,每个色块 widget 都有其对应的色块元素,且该元素都包含了 State 属性。当交换色块 widget 时,它们持有 State 属性原因相应的元素匹配不上,而期望的行为没有实现。

还是上面的步骤:
很显然,如果 W(A) 与 W(B)在交互后,canUpdate 返回 true。此时执行 child.update(newWidget),其中
child = Element(A)
child.widget = W(A)
newWidget = W(B)

不同点:因为 W(B) 中保存的不是色值而是 state 属性(人【W(B)】虽然嫁给你了,但是心【color】不属于你),所以没法更 新成功了。

在将 key 添加到色块 widget 中后,元素树和 widget 树会使用键值进行更新。当我们交换色块时,色块元素可以借助它们的 key 在 widget 树中找到它们相应的 widget,并正确地更新它们的引用,从而使 widget 正确地交换位置当按下按钮时更新其颜色。

当设置了 key 后 canUpdate = false,原因:虽然类型相同了,但是 oldWidget.key != newWidget.key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
//省略步骤....
//省略步骤...
// 我们的交换例子处理在这里
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// 如果无法更新复用,那么创建一个新的Element并返回。
return inflateWidget(newWidget, newSlot);
}

这样 updateChild 就直接执行 return inflateWidget(newWidget, newSlot); 逻辑。

既然你心不在我身上,就再找一个吧,将就在一起也没意思。

加了 key 的 W(A)和 W(B) 交换后系统更新时,不会复用原来元素树中的 Element(A) ,而是通过 W(B)重新创建一个新的 Element(B)。重新构建连带 state 中色值变量也会同步更新,达预期行为。

至此,这就是 key 如何在内部工作以及其在修改集合中有状态 widget 方面的用处。

键类型

Key 一般分两种类型:

  1. 本地类型
  2. 全局类型

本地键

在拥有相同父元素的元素中必须是独特的。本地键可以进一步分类如下:

比如同一个父节点下的孩子节点之间是独特存在的。

值键

值 Key 接受字母数字值。它们通常用于子列表中,其中每个子项的值是唯一且恒定的。

https://miro.medium.com/v2/resize:fit:1400/1*-fioksAvW553DFDTOvYFwQ.png

对象键

与值键相同,唯一的区别是它接受一个包含数据的类对象。

https://miro.medium.com/v2/resize:fit:778/1*ucNX4wjeCtyFSk2GQnBCLg.png

唯一键

在子 widget 没唯一值或根本没值的情况下,使用唯一键来标识子部件。

https://miro.medium.com/v2/resize:fit:1136/1*V5XjDNHFH19ZC8b16VeVPA.png

上面三个类型中提到的值说的是控件上承载的一些数据值。通过这些值类型来构造相对于的 Key。

页面存储键

该键用来保留用户在滚动视图中的滚动位置,以便以后可以保存。

参考链接

说说 Flutter 中最熟悉的陌生人 —— Key