大多数情况下,我们不会给
Widget
设置key
,但涉及到列表删除和插入时,通常需要使用key
,如果不使用可能会导致列表错乱
- Demo代码:Abner105/flutter-key-study
1. Key的使用示例
我们先通过以下简单的示例,我们思考一下
key
的作用是什么,以及出现这些现象的原因
1.1. StatelessWidget
BoxLessWiget
背景颜色是随机生成的StatelessWidget
是无状态的,且Widget
都是不可变的,每次都会重新构建,因此颜色会随机变化
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BoxLessWiget(title: 'box1'),
BoxLessWiget(title: 'box2'),
],
),
),
void swapBoxs() { // 点击按钮执行这个方法
setState(() {});
}
import 'package:flutter/material.dart';
import 'package:key_study/tools.dart';
class BoxLessWiget extends StatelessWidget {
const BoxLessWiget({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
print("BoxLessWiget build");
return Container(
width: 100,
height: 100,
alignment: Alignment.center,
color: getRandomColor(), // 随机生成颜色
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
}
}
- 给
StatelessWidget
加上const
,表明Widget
是常量,因此flutter
在构建时会直接复用,因此颜色不会在改变
children: [
const BoxLessWiget(title: 'box1'),
const BoxLessWiget(title: 'box2'),
],
- 我们把
children
存储到状态中,此时我们点击按钮,因为boxs
没有变化,所以色块不会变
List<Widget> boxes = [
const BoxLessWiget(title: 'box1'),
const BoxLessWiget(title: 'box2'),
];
void swapBoxs() {
setState(() {});
}
- 如果交换两个色块,颜色会随机变化
- 虽然
BoxLessWiget
是constant
,但boxes
不是constant
,所以boxs
状态变化了,重新执行build
,box
颜色就会随机刷新
void swapBoxs() {
boxes.insert(1, boxes.removeAt(0));
setState(() {});
}
- 思考:给
box
加上key
,颜色不会随机变化了,并且正常交换色块了
List<Widget> boxes = [
BoxLessWiget(
title: 'box1',
key: ValueKey(1),
),
BoxLessWiget(
title: 'box2',
key: ValueKey(2),
),
];
1.2. StatefulWidget
上面例子中,我们还可以通过
state
来管理颜色状态,这样每次biuld
就不会重新生成颜色了
- 我们将颜色保存到了
state
中了,这样每次刷新,虽然会重新执行build
,但state
中的数值没有改变,所以色块颜色不会变 - 当然如果我们加上
const
,build
都不会执行了,颜色更不可能变化了
children: [
BoxFulWiget(title: 'box1'),
BoxFulWiget(title: 'box2'),
],
void swapBoxs() {
setState(() {});
}
import 'package:flutter/material.dart';
import 'package:key_study/tools.dart';
class BoxFulWiget extends StatefulWidget {
const BoxFulWiget({
super.key,
required this.title,
});
final String title;
@override
State<BoxFulWiget> createState() => _BoxFulWigetState();
}
class _BoxFulWigetState extends State<BoxFulWiget> {
Color backgroundColor = getRandomColor(); // 先将颜色存储起来
@override
Widget build(BuildContext context) {
print("BoxFulWiget build");
return Container(
width: 100,
height: 100,
alignment: Alignment.center,
color: backgroundColor,
child: Text(
widget.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
}
}
- 接下来观察另外一个现象:我们交换两个
box
,色块没有交换
List<Widget> boxes = [
const BoxFulWiget(title: 'box1'),
const BoxFulWiget(title: 'box2'),
];
void swapBoxs() {
boxes.insert(1, boxes.removeAt(0));
setState(() {});
}
- 我们多加一个
box
,移除第一个box
,但是移除的却是最后一个
List<Widget> boxes = [
const BoxFulWiget(title: 'box1'),
const BoxFulWiget(title: 'box2'),
const BoxFulWiget(title: 'box3'),
];
void swapBoxs() {
boxes.removeAt(1);
setState(() {});
}
- 思考:给
box
加上key
后,就能正常交换了,移除也能正常移除了
List<Widget> boxes = [
BoxFulWiget(
title: 'box1',
key: ValueKey(1),
),
BoxFulWiget(
title: 'box2',
key: ValueKey(2),
),
];
void swapBoxs() {
boxes.insert(1, boxes.removeAt(0));
// boxes.removeAt(1);
setState(() {});
}
- 思考另一个现象:我们再给
BoxFulWiget
加上一个Padding
包裹,此时交换boxes
的位置,点击按钮会发现box1
的颜色随机变化了。
List<Widget> boxes = [
Padding(
padding: const EdgeInsets.all(8.0),
child: BoxFulWiget(
title: 'box1',
key: ValueKey(1),
),
),
BoxFulWiget(
title: 'box2',
key: ValueKey(2),
),
];
boxes.insert(1, boxes.removeAt(0));
2. Flutter 渲染流程
想要理解以上的现象,我们先了解一下flutter的渲染流程,key主要涉及
Widget
树和Element
树。
流程:Widget
树 => Element
树 => RenderObject
树 => paint()
绘制
2.1. Widget 树
- 不可变:
Widget
是不可变的,一旦创建无法改变。每次改变时,都会创建一个新的Widget
实例。 - 轻量级:
Widget
只是描述信息,不存储实际的布局和绘制数据,因此它非常轻量。 - 如下:
Column
、Text
和RaisedButton
都是Widget
。
Widget build(BuildContext context) {
return Column(
children: [
Text('Hello'),
RaisedButton(onPressed: () {}),
],
);
}
2.2. Element 树
- 负责管理生命周期:
Element
是Widget
的具体实现,保持Widget
和RenderObject
的生命周期。每次调用setState
或build
时,都会在Element
中进行更新。 - 如果一个
Widget
在父树中发生变化,Element
会检测到这些变化,并调用相应的布局、绘制逻辑。 - 如下: 当
MyWidget
被创建时,会产生一个Element
实例,这个Element
用来管理Column
和Text
这些Widget
。
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [Text('Hello')],
);
}
}
2.3. RenderObject 树
RenderObject
树是Element
树的一个子集。当Element
被创建后,Element
会创建一个对应的RenderObject
,并将其与父RenderObject
进行连接。RenderObject
会根据父节点传递的约束来计算布局,并进行绘制。
3. Element何时会重建
Element
如果销毁重新创建,会重新执行widget的生命周期,初始化新的State
3.1. key
或 runtimeType
不同时
- 以下是
flutter
源码片段,可以看到主要是对比类型runtimeType
和key
是否相同
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
- 我们再看看,
BoxFulWiget
不加key
,删除第一个widget
时,发生了什么box1
删除后,box2
和box1
的element
类型相同,因此box1
的element
不会被删除,并变成了box2
的element
- 依次对比,
box2
的element
变成了box3
的element
- 最终
box3
的element
没有对应的widget
,所以box3
的element
就被销毁了 - 所以显示的是box2-color1,box3-color2
- 看看上面给
BoxFulWiget
加上key
后,为什么能正常交换了- 交换位置后
box2
与box1
的element
的key
不相同,再往下对比 - 找到
box2
对应的element
,因此box2
的element
往上插入 box1
找到box1
,最终实现位置交换
- 交换位置后
3.2. 同层级对比
- 再思考:为什么加上
padding
后,box1
的颜色随机变化了- 交换位置后
box2
与Padding
的类型不一致,且Padding
没有key
,因此Padding Element
就会被销毁掉 box2
在往下找到了box2
的element
,并且key
一致,因此box2
的element
往上移Padding Widget
再对比时,没有找到对应的element
,因此会重新创建一个,那box1的颜色就会重新生成
- 交换位置后
4. Key
的类型
4.1. Globalkey
全局key
- 作用:
GlobalKey
可以跨Widget
树访问State
。 - 特点: 唯一,性能开销较大
- 常见用途:
- 访问
Scaffold
:比如控制Scaffold
的showSnackBar()
或openDrawer()
等方法。 - 表单验证:你可以使用
GlobalKey
来获取Form
的State
,例如在用户提交表单时进行验证。
- 访问
- 示例:上面示例中,给
BoxFulWiget
,加上paddind
后颜色随机变了,但此时我们把ValueKey
换成GlobalKey()
,我们发现切换位置,box1
的颜色就不会随机变化了
List<Widget> boxes = [
Padding(
padding: const EdgeInsets.all(8.0),
child: BoxFulWiget(
title: 'box1',
key: GlobalKey(), // GlobalKey可以跨Widget树进行对比
),
),
BoxFulWiget(
title: 'box2',
key: ValueKey(2),
),
];
4.2. LocalKey
局部key
Localkey
是一个抽象类,不能直接实例化,他主要有以下三种类型
4.2.1. ValueKey
- 根据某个固定值区分
Widget
,通常适用在列表中,使用id
,name
等字段来确定key
- 示例:
List<Widget> boxes = [
Padding(
padding: const EdgeInsets.all(8.0),
child: BoxFulWiget(
title: 'box1',
key: ValueKey(1),
),
),
BoxFulWiget(
title: 'box2',
key: ValueKey(2),
),
];
4.2.2. UniqueKey
- 每次生成不同的唯一
Key
,每次更新都会生成不同的key
- 以下情况,
key
在对比时,是不相同的
var key1 = UniqueKey()
var key2 = UniqueKey() // key1和key2是两个key
- 比如在以下情况下,每次点击按钮时,颜色都会随机生成
void swapBoxs() {
boxes = [
BoxFulWiget(
title: 'box2',
key: UniqueKey(),
),
BoxFulWiget(
title: 'box1',
key: UniqueKey(),
),
];
setState(() {});
}
4.2.3. ObjectKey
- 基于对象引用区分
Widget
ObjectKey
是用对象的引用来创建唯一Key
。它的比较的是对象的引用地址,而不是对象的内容。- 与
ValueKey
如何选择:
-
- 当你需要使用某个值的内容(比如
id
,name
等)来唯一标识Widget
时,ValueKey
是最合适的选择。 - 当你需要根据对象的引用来唯一标识
Widget
,或者你正在操作的对象的值会发生变化,但对象引用本身不变时,ObjectKey
更适合。
- 当你需要使用某个值的内容(比如