大多数情况下,我们不会给
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更适合。
- 当你需要使用某个值的内容(比如