0%

Flutter 中 Key 的作用

key 的定义

Key Class 官方介绍:

A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.

A new widget will only be used to update an existing element if its key is

the same as the key of the current widget associated with the element.

{@youtube 560 315 https://www.youtube.com/watch?v=kn0EOS-ZiIc}

Keys must be unique amongst the [Element]s with the same parent.

Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].

翻译过来:

一个 Key 是 Widget,Element 以及 SemanticsNode 的标识。

一个新 widget 将仅用来更新一个已存在的 element 假如它的 key 和当前 widget 关联的元素一致。

官方介绍视频 https://www.youtube.com/watch?v=kn0EOS-ZiIc

在有着相同父节点的 element 中,Key 必须是唯一的。

Key 的子类要么是 LocalKey,要么是 GlobalKey。

Key 官方介绍:

Controls how one widget replaces another widget in the tree.

If the runtimeType and key properties of the two widgets are operator==, respectively, then the new widget replaces the old widget by updating the underlying element (i.e., by calling Element.updatewith the new widget). Otherwise, the old element is removed from the tree, the new widget is inflated into an element, and the new element is inserted into the tree.

In addition, using a GlobalKey as the widget’s key allows the element to be moved around the tree (changing parent) without losing state. When a new widget is found (its key and type do not match a previous widget in the same location), but there was a widget with that same global key elsewhere in the tree in the previous frame, then that widget’s element is moved to the new location.

Generally, a widget that is the only child of another widget does not need an explicit key.

翻译过来:

控制一个小部件如何替换树中的另一个小部件。

如果两个 widget 的runtimeTypekey属性分别是相等的(==),则新 widget 通过更新基础 element(即,通过使用新的 widget 调用Element.update)来替换旧 widget。否则,将从树中删除旧 element,将新 widget 放大为一个 element,然后将新 element 插入到树中。

另外,使用GlobalKey作为窗口小部件的key允许该 element 在树上移动(更改父级)而不会丢失状态。当找到新的 widget(其键和类型与相同位置的先前 widget 不匹配),但是在前一帧的树中其他位置有一个具有相同全局键的 widget 时,该 widget 的 element 将移至新位置。

通常,作为另一个 widget 的唯一 child 的 widget 不需要显式 key。

Key 的作用

大多数时候并不需要使用 key。

当需要在一个StatefulWidget集合中进行添加、删除、重排序等操作时,才是 key 登场的时候。

无状态组件

下面这段代码在一个 Row 中展示了两个彩色方片(StatelessContainer),当点击按钮时,会交换两个方片的位置:

img_key_screen

代码如下

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
import 'dart:math';
import 'package:flutter/material.dart';

class StatelessContainer extends StatelessWidget {
final Color color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}

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(() {});
}
}

有状态组件

有状态组件的状态信息(如颜色)通常是存储在 state 中的,而 state 是存储在 element 树中的。

那么 Key 到底应该用到哪呢?
我们再来一个例子,我们把色块用 Padding 包装一下。运行之后会发现,色块并没有交换,而是以随机的形式在变换颜色。为什么呢?

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
import 'dart:math';
import 'package:flutter/material.dart';

class Screen extends StatefulWidget {
Screen({Key key}) : super(key: key);

@override
_ScreenState createState() => _ScreenState();
}

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()),
),
];

@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(() {});
print('${widgets[0]}, ${widgets[1]}');
}
}

class StatefulContainer extends StatefulWidget {
StatefulContainer({Key key}) : super(key: key);

@override
_StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
final Color color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

@override
Widget build(BuildContext context) {
return Container(
color: color,
width: 100,
height: 100,
);
}
}

结合我们上面的理论,我们分析一下这次的 Widget Tree 和 Element Tree,当我们交换元素后,Flutter element-to-widget matching algorithm,(元素-组件匹配算法),开始进行对比,算法每次只对比一层,即 Padding 这一层。显然,Padding 并没有发生本质的变化。

于是开始进行第二层对比,在对比时 Flutter 发现元素与组件的 Key 并不匹配,于是,把它设置成不可用状态,但是这里所使用的 Key 只是本地 Key(Local Key),Flutter 并不能找到另一层里面的 Key(即另外一个 Padding Widget 中的 Key)所以,Flutter 就创建了一个新的 Widget,而这个 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
75
76
77
78
79
80
81
82
import 'dart:math';

import 'package:flutter/material.dart';

class Screen extends StatefulWidget {
Screen({Key key}) : super(key: key);

@override
_ScreenState createState() => _ScreenState();
}

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(),
),
];

@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(() {});
print('${widgets[0]}, ${widgets[1]}');
}
}

class StatelessContainer extends StatelessWidget {
final Color color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

@override
Widget build(BuildContext context) {
return Container(
color: color,
width: 100,
height: 100,
);
}
}

class StatefulContainer extends StatefulWidget {
StatefulContainer({Key key}) : super(key: key);

@override
_StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
final Color color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

@override
Widget build(BuildContext context) {
return Container(
color: color,
width: 100,
height: 100,
);
}
}

Key 的分类

img_key

  • ValueKey:以一个值为 key。
  • ObjectKey:以一个对象为 key。
  • UniqueKey:生成唯一的随机数作为 key。
  • PageStorageKey:专用于存储页面滚动位置的 key。
  • GlobalKey:见后文。

何时使用 key

ValueKey

如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。

这时候就需要使用 ValueKey!

1
2
3
4
5
6
7
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 都是一个在整个应用内唯一的 key。

globalkey 相对而言是比较昂贵的,如果你并不需要 globalkey 的某些特性,那么可以考虑使用 Key、ValueKey、ObjectKey 或 UniqueKey。

用途 1

允许 widget 在应用程序中的任何位置更改其 parent 而不丢失其状态。应用场景:在两个不同的屏幕上显示相同的 widget,并保持状态相同。

用途 2

GlobalKey 能够跨 Widget 访问状态。 在这里我们有一个 Switcher 小部件,它可以通过 changeState 改变它的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 并传递给 SwitcherScreen。然后我们便可以通过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。

参考资料