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),当点击按钮时,会交换两个方片的位置:

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

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}

class StatelessContainer extends StatelessWidget {

StatelessContainer({Key? key, Color? color}) : super(key: key);
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 {
const Screen({Key? key}) : super(key: key);

@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: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

代码运行效果符合我们的预期。但是,如果我们把色块 WidgetStatelessWidget 变更为 StatefulWidget,并把颜色属性存储在 State 中,那么情况又如何呢?此时发现,无论我们怎么点击交换按钮,色块的位置或者颜色都不会再交换了。

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
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}
class StatelessContainer extends StatefulWidget{
const StatelessContainer({Key? key}) : super(key: key);

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

class _StatelessContainerState extends State {
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 {
const Screen({Key? key}) : super(key: key);

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

class _ScreenState extends State<Screen> {
List<Widget> widgets = [
const StatelessContainer(),
const StatelessContainer(),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

但是如果将颜色的属性存储在 Widget 中而不是 State 中,那么此时的交换效果又变得正常了。

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
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}
class StatelessContainer extends StatefulWidget{

const StatelessContainer({Key? key, required this.color}) : super(key: key);
final Color color;
@override
_StatelessContainerState createState() => _StatelessContainerState();
}

class _StatelessContainerState extends State<StatelessContainer> {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: widget.color,
);
}
}

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

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

class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatelessContainer(color: Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1)),
StatelessContainer(color: Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1)),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

那么,对于上面说的颜色无法交换的情况甚或其他各种类似的情况,我们在开发中该怎么处理呢?

就在此时,Key 作为一个 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
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}
class StatefulContainer extends StatefulWidget {
const StatefulContainer({required 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(
width: 100,
height: 100,
color: color,
);
}
}


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

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

class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatefulContainer(key: UniqueKey()),//加了key
StatefulContainer(key: UniqueKey()),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

两个色块又可以交换啦。

Widget 更新机制

若你还对 Element 的概念感到很模糊的话,请先阅读 Flutter | 深入理解BuildContext

下面来来看Widget的源码。

1
2
3
4
5
6
7
8
9
10
@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 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新 Widget 就可以了

StatelessContainer 比较过程

在 StatelessContainer 中,我们并没有传入 key ,所以只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,StatelessElement 调用新持有 Widget 的 build 方法重新构建,而我们的 color 实际上就是储存在 widget 中的,因此在屏幕上两个 Widget 便被正确的交换了顺序。

StatefulContainer 比较过程

而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。

当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 true,于是更新 StatefulWidget 的位置,这两个 Element 将不会交换位置。但是原有 Element 只会从它持有的 state 实例中 build 新的 widget。因为 element 没变,它持有的 state 也没变。所以颜色不会交换。这里变换 StatefulWidget 的位置是没有作用的。

而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 false。(这里 runtimeType 相同,key 不同)

此时 RenderObjectElement 会用新 Widget 的 key 在老 Element 列表里面查找,找到匹配的则会更新 Element 的位置并更新对应 renderObject 的位置,对于这个例子来讲就是交换了 Element 的位置并交换了对应 renderObject 的位置。都交换了,那么颜色自然也就交换了。

比较范围

为了提升性能 Flutter 的比较算法(diff)是有范围的,它并不是对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。

在这个例子中,我们将两个带 key 的 StatefulContainer 包裹上 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
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';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}
class StatefulContainer extends StatefulWidget {
const StatefulContainer({required 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(
width: 100,
height: 100,
color: color,
);
}
}


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

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

class _ScreenState extends State<Screen> {//套了padding
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: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

两个 Widget 的 Element 并不是交换顺序,而是被重新创建了。

在 Flutter 的比较过程中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。然后它会对所有 children 层逐个进行扫描。

在Column这一层级,padding 部分的 runtimeType 并没有改变,且不存在 Key。然后再比较下一个层级。由于内部的 StatefulContainer 存在 key,新旧 key 不同(UniqueKey使然),且现在的层级在 padding 内部,该层级没有多子 Widget。canUpdate 返回 flase,Flutter 的将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。第二个 Widget 同理

所以为了解决这个问题,我们需要将 key 放到 Row 的 children 这一层级。

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
83
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Key Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Screen(),
);
}
}
class StatefulContainer extends StatefulWidget {
@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(
width: 100,
height: 100,
color: color,
);
}
}


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

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

class _ScreenState extends State<Screen> {
List<Widget> widgets = [
Padding(
key: UniqueKey(),//here
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: const Icon(Icons.undo),
),
);
}

switchWidget() {
widgets.insert(1, widgets.removeAt(0));
setState(() {});
}
}

key的分类

Key

1
2
3
4
5
6
7
@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

1
2
3
4
5
6
7
8
9
10
@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的 WidgetStateElement

注意: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 能够跨 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 改变状态了。

参考

https://api.flutter.dev/flutter/foundation/Key-class.html

https://medium.flutterdevs.com/keys-in-flutter-104fc01db48f

https://zhuanlan.zhihu.com/p/346892355

https://juejin.cn/post/6844903811870359559