0%

理解flutter中的Key

我们知道,flutter有三颗树,widget树在每次setState的时候都会重建,而element树不会,而是会通过diff算法,计算出哪些element需要重建,哪些element可以重用,我们通过一个例子来开始

例子

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

class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}

final _random = Random();

class _HomePageState extends State<HomePage> {
var _items = [
ListItem(title: "aaa"),
ListItem(title: "bbb"),
ListItem(title: "ccc"),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("key demo"),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _items,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 删除第一个元素
_items.removeAt(0);
setState(() {});
},
),
);
}
}

/// 定义一个item
class ListItem extends StatelessWidget {
final String title;
// color放在widget
final Color color = Color.fromARGB(
255, _random.nextInt(256), _random.nextInt(256), _random.nextInt(256));

ListItem({this.title});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(
this.title,
style: TextStyle(color: Colors.white, fontSize: 20),
),
color: this.color,
width: 100,
height: 100,
);
}
}

运行正常,接下来我们把ListItem换成stateful

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
class ListItem extends StatefulWidget {
final String title;

ListItem({this.title});
@override
_ListItemState createState() => _ListItemState();
}

class _ListItemState extends State<ListItem> {
// color放在state
final Color color = Color.fromARGB(
255, _random.nextInt(256), _random.nextInt(256), _random.nextInt(256));

@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(
widget.title,
style: TextStyle(color: Colors.white, fontSize: 20),
),
color: this.color,
width: 100,
height: 100,
);
}
}

从上图可以看出,颜色和文字混了,这是由于element树判断增量更新重用element导致的

当widget重建的时候,element通过对比新旧两个widget是否需要更新,从而判断是否重用,默认的逻辑是对比runtimeTypekey,我们上面的例子中显然会返回true(我们没有定义key),则表示可以element可以直接使用新的widget

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

由于颜色是state持有的,没有变化,所以蓝色的element,直接使用了灰色的widget,而最后一个绿色的element,没有用到,会被释放

通过canUpdate方法可以看到,我们可以设置key来标识element是否可以直接更新widget,flutter中的key有两种

  • LocalKey
  • GlobalKey

LocalKey

LocalKey有下面三种,其成员key用于比较,使用起来类似

  • ValueKey: 使用一个泛型数据作为key
  • ObjectKey: 使用一个对象作为key
  • UniqueKey: 自动生成key,并且保证唯一,比较少用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在StatefulWidget构造方法添加参数key,并传给super
class ListItem extends StatefulWidget {
final String title;

ListItem({this.title, Key key}) : super(key: key);
@override
_ListItemState createState() => _ListItemState();
}

// 使用ListItem的时候,传入key
var _items = [
ListItem(title: "aaa", key: ValueKey<String>("aaa")),
ListItem(title: "bbb", key: ValueKey<String>("bbb")),
ListItem(title: "ccc", key: ValueKey<String>("ccc")),
];

这样在判断的时候不同的ListItem会在canUpdate方法就会返回false,就不会重用了

通常我们在自定义StatefulWidget的时候,需要在构造函数添加可选参数key

GlobalKey

GlobalKey可以获取到context(element),widget,和state,通常用于在父widget操作子widget

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

class HomePage extends StatelessWidget {
final GlobalKey<_TestWidgetState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("global key"),
),
body: Center(
child: TestWidget(
key: _globalKey,
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 直接操作子widget的state
_globalKey.currentState.increseCount();
},
),
);
}
}

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

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

class _TestWidgetState extends State<TestWidget> {
int _count = 0;

increseCount() {
setState(() {
_count = _count + 1;
});
}

@override
Widget build(BuildContext context) {
return Text("$_count");
}
}