Flutter Key 详解:底层原理、类型区别与实际使用场景
一、先理解 Key 到底解决什么问题
一句话概括:
Key 用来告诉 Flutter:新一轮 build 生成的 Widget,应该与之前的哪个 Element 和 State 对应。
Flutter UI 主要涉及三类对象:
- Widget:不可变的配置描述,每次 build 都可能重新创建。
- Element:Widget 在组件树中的实例和位置,生命周期通常比 Widget 长。
- State:StatefulWidget 对应的可变状态,实际绑定在 StatefulElement 上。
例如:
const UserCard(
userId: 1001,
name: 'Alice',
);
UserCard Widget 本身只是配置。真正需要保留的输入框状态、动画状态、计数状态等,通常存放在它对应的 State 中。
当父组件重新 build 时,Flutter 会拿“旧 Widget”和“新 Widget”进行比较。判断逻辑可以简化为:
bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
也就是说:
runtimeType 相同
并且
Key 相同
=> 复用原来的 Element 和 State
否则:
删除旧 Element
创建新 Element
创建新的 State
Flutter 官方的 Widget.canUpdate 使用的正是 runtimeType 和 key。当两个 Widget 都没有 Key 时,只要类型相同,Flutter就可能认为它们可以互相更新。
二、不设置 Key 时,Flutter 如何匹配 Widget
假设有两个相同类型的组件:
Column(
children: const [
UserCard(name: 'Alice'),
UserCard(name: 'Bob'),
],
);
重新 build 后顺序变成:
Column(
children: const [
UserCard(name: 'Bob'),
UserCard(name: 'Alice'),
],
);
两个位置上的 Widget 都是:
runtimeType == UserCard
key == null
Flutter默认按位置匹配:
旧位置 0 的 UserCard
更新成
新位置 0 的 UserCard
旧位置 1 的 UserCard
更新成
新位置 1 的 UserCard
因此,它不会自动理解 Alice 和 Bob 交换了位置。
它看到的只是:
第一个 UserCard 还是 UserCard
第二个 UserCard 还是 UserCard
如果 UserCard 是 StatefulWidget,它的 State 可能继续留在原来的位置,而不是跟着 Alice 或 Bob 移动。
这就是列表增加、删除、排序后出现状态错位的主要原因。
三、可运行示例:相同 StatefulWidget 交换位置
下面的代码可以直接运行。
1. 错误版本:没有 Key
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: SwapDemoPage(),
),
);
}
class SwapDemoPage extends StatefulWidget {
const SwapDemoPage({super.key});
@override
State<SwapDemoPage> createState() => _SwapDemoPageState();
}
class _SwapDemoPageState extends State<SwapDemoPage> {
List<String> users = ['Alice', 'Bob'];
void swapUsers() {
setState(() {
users = users.reversed.toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Key Demo'),
),
body: Column(
children: [
ElevatedButton(
onPressed: swapUsers,
child: const Text('交换位置'),
),
...users.map(
(name) => UserCounterCard(
name: name,
),
),
],
),
);
}
}
class UserCounterCard extends StatefulWidget {
const UserCounterCard({
super.key,
required this.name,
});
final String name;
@override
State<UserCounterCard> createState() => _UserCounterCardState();
}
class _UserCounterCardState extends State<UserCounterCard> {
int count = 0;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.name),
subtitle: Text('Count: $count'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
count++;
});
},
),
);
}
}
操作步骤
- 给 Alice 点击几次加号。
- 点击“交换位置”。
- Alice 和 Bob 的顺序交换。
- 计数可能仍留在原来的位置。
问题原因
Flutter看到的结构是:
位置 0:UserCounterCard
位置 1:UserCounterCard
交换后仍然是:
位置 0:UserCounterCard
位置 1:UserCounterCard
由于类型相同并且都没有 Key,Flutter复用了原位置上的 State。
2. 正确版本:添加 ValueKey
修改列表构建代码:
...users.map(
(name) => UserCounterCard(
key: ValueKey(name),
name: name,
),
),
现在 Flutter 可以识别:
ValueKey('Alice')
ValueKey('Bob')
交换位置后,Alice 对应的 State 会跟着 Alice 移动,而不是留在原位置。
四、Flutter Key 的分类关系
Flutter Key 大致分成两类:
Key
├── LocalKey
│ ├── ValueKey
│ │ └── PageStorageKey
│ ├── ObjectKey
│ └── UniqueKey
│
└── GlobalKey
├── LabeledGlobalKey
└── GlobalObjectKey
LocalKey 只要求在同一个父节点的直接子节点中保持唯一;GlobalKey 则必须在整个应用当前组件树中保持唯一。
五、Key
1. Key 是什么
Key 是所有 Flutter Key 的基础抽象类。
abstract class Key
一般不会直接继承或实例化抽象类,但 Flutter 提供了一个字符串构造方式:
const Key('user-1001')
它实际上等价于:
const ValueKey<String>('user-1001')
官方实现是:
const factory Key(String value) = ValueKey<String>;
2. 使用场景
适合简单的字符串标识:
ListTile(
key: const Key('settings-notification-item'),
title: const Text('Notifications'),
);
3. 不适合的场景
当标识值不是字符串时,应该明确使用:
ValueKey<int>(user.id)
而不是先转字符串:
Key(user.id.toString())
虽然两种写法都能工作,但 ValueKey<int> 类型信息更加明确。
4. 风险
Key 本身没有特殊性能风险。
真正的问题通常是:
- Key 不稳定;
- Key 重复;
- Key 放错层级;
- 使用会不断变化的值作为 Key。
六、ValueKey
1. 作用
ValueKey<T> 使用一个值判断两个 Key 是否相同。
const ValueKey<int>(1001)
两个 ValueKey 相等,需要满足:
Key 类型相同
并且
value == other.value
例如:
const ValueKey<int>(1) == const ValueKey<int>(1)
结果为:
true
ValueKey 使用传入值的 == 和 hashCode 判断身份。
2. 最适合的场景
- 数据库 ID;
- 用户 ID;
- 消息 ID;
- 商品 ID;
- 稳定且唯一的业务字段;
- AnimatedSwitcher 的内容变化;
- ReorderableListView;
- 可以增加、删除或排序的列表。
3. 推荐写法
ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserCard(
key: ValueKey(user.id),
user: user,
);
},
);
4. 不推荐使用索引作为 Key
错误:
UserCard(
key: ValueKey(index),
user: user,
);
假设原列表:
index 0 -> Alice
index 1 -> Bob
index 2 -> Carol
删除 Alice 后:
index 0 -> Bob
index 1 -> Carol
Bob 的 Key 从:
ValueKey(1)
变成:
ValueKey(0)
这不是稳定身份。
正确:
UserCard(
key: ValueKey(user.id),
user: user,
);
5. 使用可变值的风险
错误:
UserCard(
key: ValueKey(user.name),
user: user,
);
如果用户修改名字,Key 也会变化:
Alice -> Alicia
Flutter会认为原来的组件被删除,并创建一个新组件,导致:
- State 重置;
- 输入内容丢失;
- 动画重新开始;
- Controller 重新创建;
- 焦点丢失。
应该使用稳定 ID:
ValueKey(user.id)
6. 性能和内存风险
ValueKey 本身很轻量。
主要风险不是内存泄漏,而是业务值不稳定导致错误地销毁和重建 State。
七、ObjectKey
1. 作用
ObjectKey 使用对象实例本身作为身份。
ObjectKey(user)
它比较的不是普通的 ==,而是:
identical(oldObject, newObject)
也就是说,必须是内存中的同一个对象实例。
2. 示例
final user = User(
id: 1001,
name: 'Alice',
);
UserCard(
key: ObjectKey(user),
user: user,
);
只要每次 build 使用的仍然是同一个 user 对象,Key 就相同。
3. ObjectKey 与 ValueKey 的区别
假设有两个内容相同但实例不同的对象:
final user1 = User(id: 1001, name: 'Alice');
final user2 = User(id: 1001, name: 'Alice');
使用:
ObjectKey(user1)
ObjectKey(user2)
结果不同,因为:
identical(user1, user2) == false
使用:
ValueKey(user1.id)
ValueKey(user2.id)
结果相同,因为:
user1.id == user2.id
4. 适合场景
适合:
- 对象实例本身代表身份;
- 列表长期复用同一批 Model 实例;
- 数据对象没有稳定 ID;
- 不同对象即使内容相同,也必须被视为不同项目。
5. 不适合场景
很多 API 和状态管理框架会不断创建新的 Model:
User.fromJson(json)
即使用户 ID 没变,每次刷新后也会得到新的对象实例。
此时使用:
ObjectKey(user)
Key 每次都会变化。
这种情况应该使用:
ValueKey(user.id)
6. 风险
主要风险是误以为它根据对象内容比较。
实际上:
旧对象和新对象内容一样
但不是同一个实例
=> ObjectKey 不相同
=> State 可能被重建
八、UniqueKey
1. 作用
每次执行:
UniqueKey()
都会创建一个只与自身相等的 Key。
final key1 = UniqueKey();
final key2 = UniqueKey();
print(key1 == key2); // false
UniqueKey 不能使用 const,因为如果是 const,Dart可能复用相同实例,这会违背“每个 Key 都唯一”的设计。
2. 适合场景
适合没有业务 ID,但需要给当前对象生成一次性唯一身份的场景。
例如:
class TodoItem {
TodoItem({
required this.title,
}) : key = UniqueKey();
final String title;
final Key key;
}
使用:
TodoRow(
key: todo.key,
todo: todo,
);
注意:UniqueKey 存在 Model 中,因此不会在每次 build 时重新生成。
3. 强制重置子组件
class ResetDemo extends StatefulWidget {
const ResetDemo({super.key});
@override
State<ResetDemo> createState() => _ResetDemoState();
}
class _ResetDemoState extends State<ResetDemo> {
Key childKey = UniqueKey();
void resetChild() {
setState(() {
childKey = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterPanel(
key: childKey,
),
ElevatedButton(
onPressed: resetChild,
child: const Text('重置子组件'),
),
],
);
}
}
每次点击按钮才创建新的 UniqueKey,因此 Flutter 会删除旧 State 并创建新 State。
4. 严重错误:在 build 中直接创建
错误:
@override
Widget build(BuildContext context) {
return UserCard(
key: UniqueKey(),
user: user,
);
}
每次父组件 build:
旧 UniqueKey != 新 UniqueKey
Flutter会认为这是一个全新的 Widget。
可能导致:
- State 每次被重置;
- 输入框内容丢失;
- 列表闪烁;
- 动画重复开始;
- 焦点丢失;
- 性能下降。
5. 正确写法
在 State 中只创建一次:
late final Key userCardKey;
@override
void initState() {
super.initState();
userCardKey = UniqueKey();
}
然后:
UserCard(
key: userCardKey,
user: user,
);
九、GlobalKey
1. 作用
GlobalKey 是整个应用当前组件树中全局唯一的 Key。
它主要提供三个特殊能力:
- 获取 Widget 对应的 State;
- 获取 BuildContext;
- 在组件树中移动子树时保留其 Element 和 State。
final key = GlobalKey<MyWidgetState>();
key.currentState;
key.currentContext;
key.currentWidget;
Flutter 官方说明,GlobalKey 能全局识别 Element,并可通过 currentState、currentContext 和 currentWidget 访问相关对象。
2. 表单验证场景
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
void submit() {
final isValid = formKey.currentState?.validate() ?? false;
if (!isValid) {
return;
}
formKey.currentState?.save();
}
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your email.';
}
return null;
},
),
ElevatedButton(
onPressed: submit,
child: const Text('Submit'),
),
],
),
);
}
}
Flutter 官方同时支持两种获取 FormState 的方式:
Form.of(context)
或者:
GlobalKey<FormState>.currentState
如果按钮位于 Form 的子树中,通常可以考虑:
Form.of(context)
这样可以避免额外使用 GlobalKey。
3. 父组件调用子组件方法
class ParentPage extends StatefulWidget {
const ParentPage({super.key});
@override
State<ParentPage> createState() => _ParentPageState();
}
class _ParentPageState extends State<ParentPage> {
final GlobalKey<CounterBoxState> counterKey =
GlobalKey<CounterBoxState>();
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterBox(
key: counterKey,
),
ElevatedButton(
onPressed: () {
counterKey.currentState?.increment();
},
child: const Text('父组件增加计数'),
),
],
);
}
}
class CounterBox extends StatefulWidget {
const CounterBox({
super.key,
});
@override
State<CounterBox> createState() => CounterBoxState();
}
class CounterBoxState extends State<CounterBox> {
int count = 0;
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return Text('Count: $count');
}
}
4. 是否推荐父组件直接调用子组件方法
技术上可以,但不应该成为默认的组件通信方式。
普通业务状态更适合使用:
- 回调;
- ValueNotifier;
- ChangeNotifier;
- Bloc;
- Provider;
- Riverpod;
-共享 ViewModel。
GlobalKey 更适合:
- FormState;
- ScaffoldMessenger 等框架状态;
- 特殊组件控制;
- 获取组件尺寸和位置;
- 必须直接操作某个 State 的少量场景。
5. 获取组件位置和大小
final GlobalKey boxKey = GlobalKey();
void printSizeAndPosition() {
final context = boxKey.currentContext;
if (context == null) {
return;
}
final renderObject = context.findRenderObject();
if (renderObject is! RenderBox) {
return;
}
final size = renderObject.size;
final position = renderObject.localToGlobal(Offset.zero);
debugPrint('size: $size');
debugPrint('position: $position');
}
组件:
Container(
key: boxKey,
width: 200,
height: 100,
child: const Text('Target'),
);
应当在布局完成后读取:
WidgetsBinding.instance.addPostFrameCallback((_) {
printSizeAndPosition();
});
因为在 build 期间,RenderObject 可能还没有完成当前帧布局。
6. 为什么不要大量使用 GlobalKey
GlobalKey 的匹配范围是整个应用组件树,而不是同一个父节点。
当带 GlobalKey 的组件从一个位置移动到另一个位置时,Flutter可以把整个子树重新挂载到新位置并保留 State。但是这个过程相对昂贵,会触发相关 State 的 deactivate,并可能使依赖 InheritedWidget 的子组件重新 build。
因此,当只需要列表身份时,不要使用:
GlobalKey()
应该使用:
ValueKey(item.id)
7. 严重错误:每次 build 创建 GlobalKey
错误:
@override
Widget build(BuildContext context) {
final formKey = GlobalKey<FormState>();
return Form(
key: formKey,
child: const Placeholder(),
);
}
每次 build 都会得到新的 GlobalKey。
结果可能包括:
- 原子树 State 被丢弃;
- 表单状态重置;
- 手势中断;
- 组件重新初始化;
- 不必要的性能损耗。
Flutter 官方明确建议 GlobalKey 由 State 长期持有,并在 build 外创建,例如字段初始化或 initState。
正确:
class _PageState extends State<Page> {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: const Placeholder(),
);
}
}
8. 重复使用同一个 GlobalKey
错误:
final key = GlobalKey();
Column(
children: [
Container(key: key),
Container(key: key),
],
);
两个组件同时存在于组件树中,但使用了同一个 GlobalKey。
Flutter会在运行时抛出断言错误,因为同一个 GlobalKey 不能同时绑定两个 Element。
跨页面共享 GlobalKey 同样需要注意。
例如 Page A 尚未完全移除,Page B 已经开始构建,在页面切换动画期间两个页面可能同时存在于组件树中。这时如果两个页面使用同一个 GlobalKey,也可能发生冲突。
9. GlobalKey 是否会导致内存泄漏
GlobalKey 本身不等于内存泄漏。
更常见的问题是:
- 把 GlobalKey 放在长期存在的静态变量或单例中;
- 长期保存
currentContext或currentState; - State 中的 Controller、Stream、AnimationController 没有 dispose;
- GlobalKey 的生命周期远长于页面实际生命周期。
推荐:
class _PageState extends State<Page> {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
}
不推荐无必要地定义成:
static final GlobalKey<FormState> formKey =
GlobalKey<FormState>();
Key 应该由真正拥有它的页面或组件负责管理。
十、GlobalObjectKey
1. 作用
GlobalObjectKey 是全局版的 ObjectKey。
它使用对象实例作为全局身份:
GlobalObjectKey(user)
任何使用同一个对象实例创建的 GlobalObjectKey 都会匹配。
它比较的是:
identical(oldValue, newValue)
而不是普通的 ==。
2. 示例
class UserEditor extends StatefulWidget {
const UserEditor({
super.key,
required this.user,
});
final User user;
@override
State<UserEditor> createState() => UserEditorState();
}
class UserEditorState extends State<UserEditor> {
void save() {
debugPrint('Save ${widget.user.name}');
}
@override
Widget build(BuildContext context) {
return Text(widget.user.name);
}
}
使用:
final User alice = User(
id: 1001,
name: 'Alice',
);
UserEditor(
key: GlobalObjectKey<UserEditorState>(alice),
user: alice,
);
之后只要仍然使用同一个 alice 实例,就可以通过同等的 GlobalObjectKey 找到对应 State。
3. 更安全的私有 Key 类型
不同模块可能误用同一个对象创建 GlobalObjectKey,从而发生全局冲突。
官方建议可以创建私有子类:
class _UserEditorKey
extends GlobalObjectKey<UserEditorState> {
const _UserEditorKey(super.value);
}
使用:
UserEditor(
key: _UserEditorKey(alice),
user: alice,
);
因为 Key 的 runtimeType 也是其身份的一部分,私有子类可以降低不同模块之间发生冲突的风险。
4. 不适合场景
如果只是普通列表排序,不需要全局访问 State:
ValueKey(user.id)
通常比:
GlobalObjectKey(user)
更简单、更安全。
十一、PageStorageKey
1. 作用
PageStorageKey 用于在最近的 PageStorage 中保存和恢复部分页面状态。
最常见的是:
- 列表滚动位置;
- Tab 中不同列表的滚动位置;
- 页面切换后恢复 ScrollView 位置;
- ExpansionTile 展开状态等支持 PageStorage 的组件状态。
它本质上继承自:
ValueKey<T>
Flutter 官方说明,PageStorageKey 的值及其祖先 PageStorageKey 链必须在最近的 PageStorage 中保持唯一,并且 Key 的值不能在每次重建时改变身份。
2. 多个 Tab 的列表
class ListTab extends StatelessWidget {
const ListTab({
super.key,
required this.tabId,
});
final String tabId;
@override
Widget build(BuildContext context) {
return ListView.builder(
key: PageStorageKey<String>('list-$tabId'),
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('$tabId item $index'),
);
},
);
}
}
使用:
TabBarView(
children: const [
ListTab(tabId: 'likes'),
ListTab(tabId: 'visitors'),
ListTab(tabId: 'messages'),
],
);
每个列表都有不同的 PageStorageKey:
list-likes
list-visitors
list-messages
切换 Tab 后,Flutter可以区分并恢复不同列表的滚动位置。
ScrollView 默认会尝试通过 PageStorage 保存滚动位置。当同一个页面存在多个 ScrollView 时,官方建议使用 PageStorageKey 区分它们。
3. 错误写法
ListView(
key: PageStorageKey(DateTime.now()),
children: children,
);
每次 build 的值都不同,旧状态无法被找到。
正确:
const PageStorageKey<String>('message-list')
4. PageStorageKey 与持久化的区别
PageStorageKey 不是:
- 数据库;
- SharedPreferences;
- 用户账号云同步;
- 完整的进程重启状态恢复方案。
它主要解决当前 Widget/PageStorage 生命周期内的页面状态恢复。
如果需要应用被系统杀死后恢复状态,应进一步研究 Flutter 的 State Restoration 和 restorationId。
十二、LabeledGlobalKey
1. 作用
LabeledGlobalKey 是带调试标签的 GlobalKey。
final key = GlobalKey<FormState>(
debugLabel: 'loginForm',
);
这里创建出来的实际实现就是 LabeledGlobalKey。
标签只用于:
- 调试;
- 日志;
- DevTools;
- 错误信息;
- 区分多个 GlobalKey。
标签不会参与 Key 身份比较。即使两个标签相同,也仍然是两个不同的 GlobalKey。
2. 示例
final GlobalKey<FormState> loginFormKey =
GlobalKey<FormState>(
debugLabel: 'login-form',
);
final GlobalKey<FormState> registerFormKey =
GlobalKey<FormState>(
debugLabel: 'register-form',
);
3. 相同标签不代表相同 Key
final key1 = GlobalKey(debugLabel: 'form');
final key2 = GlobalKey(debugLabel: 'form');
print(key1 == key2); // false
标签只是为了方便开发人员看懂:
这个 GlobalKey 属于哪个页面或组件
4. 使用建议
一般不需要直接写:
LabeledGlobalKey<FormState>('login-form')
更常见的写法是:
GlobalKey<FormState>(
debugLabel: 'login-form',
)
十三、AnimatedSwitcher 为什么需要 Key
错误示例
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
'$count',
),
);
虽然文本内容从:
1
变成:
2
但是旧组件和新组件都是:
runtimeType == Text
key == null
Flutter认为它仍然是同一个 Widget 的参数更新,因此可能不会执行预期的切换动画。
正确写法
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
'$count',
key: ValueKey<int>(count),
),
);
当 count 改变:
ValueKey(1) != ValueKey(2)
AnimatedSwitcher 会把它识别为新的 child,并执行进入和退出动画。Flutter 官方也建议在需要区分 AnimatedSwitcher 子组件时使用 Key,通常是基于数据的 ValueKey。
十四、ReorderableListView 中的 Key
ReorderableListView 要求所有列表项都有 Key。
正确示例
class Todo {
const Todo({
required this.id,
required this.title,
});
final int id;
final String title;
}
ReorderableListView(
children: todos.map((todo) {
return ListTile(
key: ValueKey(todo.id),
title: Text(todo.title),
);
}).toList(),
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex--;
}
final item = todos.removeAt(oldIndex);
todos.insert(newIndex, item);
});
},
);
错误示例
key: ValueKey(index)
拖动后,index 代表位置,而不是数据身份。
正确:
key: ValueKey(todo.id)
十五、Key 应该添加在哪一层
一个重要原则:
Key 应该放在发生增加、删除或顺序变化的那组直接子节点上。
错误示例
Column(
children: users.map((user) {
return Padding(
padding: const EdgeInsets.all(8),
child: UserCard(
key: ValueKey(user.id),
user: user,
),
);
}).toList(),
);
在 Column 看来,它的直接子节点是:
Padding
Padding
Padding
这些 Padding 都没有 Key。
Key 放在更里面的 UserCard 上,无法帮助 Column 正确识别哪个直接子节点发生了移动。
正确写法
Column(
children: users.map((user) {
return Padding(
key: ValueKey(user.id),
padding: const EdgeInsets.all(8),
child: UserCard(
user: user,
),
);
}).toList(),
);
也可以使用:
KeyedSubtree(
key: ValueKey(user.id),
child: Padding(
padding: const EdgeInsets.all(8),
child: UserCard(
user: user,
),
),
);
判断方法
问自己:
哪一组兄弟节点会发生顺序变化?
Key 就应该放在这组兄弟节点的最外层。
十六、ValueKey、ObjectKey 和 UniqueKey 如何选择
有稳定业务 ID
使用:
ValueKey(item.id)
这是绝大多数列表场景的首选。
没有 ID,但对象实例长期稳定
使用:
ObjectKey(item)
前提是每次 build 仍然使用同一个对象实例。
没有 ID,也没有合适的对象身份
创建一次:
final key = UniqueKey();
然后长期保存这个 Key。
想强制重置 State
在需要重置的时刻创建新的:
setState(() {
childKey = UniqueKey();
});
不要在每次 build 中创建。
十七、LocalKey 和 GlobalKey 的本质区别
| 对比项 | LocalKey | GlobalKey |
|---|---|---|
| 唯一范围 | 同一个父节点的直接子节点 | 整个应用当前组件树 |
| 常见类型 | ValueKey、ObjectKey、UniqueKey | GlobalKey、GlobalObjectKey |
| 主要目的 | 列表匹配、排序、删除、动画 | 获取 State/Context、跨位置移动子树 |
| 性能成本 | 通常较低 | 相对较高 |
| 是否可访问 State | 不可以 | 可以 |
| 是否可访问 Context | 不可以 | 可以 |
| 是否允许跨父节点保留子树 | 不可以 | 可以 |
| 是否适合普通列表 | 是 | 通常不适合 |
十八、const Widget 与 Key 的关系
1. const 不能代替 Key
const UserCard(
name: 'Alice',
)
const 的主要作用是让不可变 Widget 可以在编译期创建和复用,减少重复对象创建。
但它没有表达:
Alice 是哪个业务对象
列表顺序变化时,仍然需要:
const UserCard(
key: ValueKey('alice'),
name: 'Alice',
)
2. const ValueKey
ValueKey 支持 const:
const ValueKey<String>('alice')
只要 value 相同,它们就相等。
3. UniqueKey 不支持 const
错误:
const UniqueKey()
UniqueKey 必须保证每个实例都不同,因此没有 const 构造。
4. const 不保证 State 永远保留
是否保留 State,最终仍取决于:
Widget 在树中的位置
runtimeType
Key
组件树更新过程
不能简单理解为:
用了 const,State 就不会重建
十九、常见错误总结
错误一:列表使用 index 作为 Key
ValueKey(index)
问题表现
- 删除数据后状态错位;
- 输入内容跑到其他列表项;
- Switch 状态与数据不一致。
正确写法
ValueKey(item.id)
错误二:build 中创建 UniqueKey
Widget build(BuildContext context) {
return Editor(
key: UniqueKey(),
);
}
问题表现
- 每次 build 都重置;
- 输入内容丢失;
- 动画反复启动。
正确写法
final Key editorKey = UniqueKey();
错误三:build 中创建 GlobalKey
final key = GlobalKey<FormState>();
问题表现
- FormState 重置;
- 子树反复创建;
- currentState 不稳定。
正确写法
class _PageState extends State<Page> {
final key = GlobalKey<FormState>();
}
错误四:同一个 GlobalKey 同时使用两次
Column(
children: [
Form(key: formKey, child: child1),
Form(key: formKey, child: child2),
],
);
问题表现
Flutter抛出重复 GlobalKey 的运行时断言。
正确写法
final firstFormKey = GlobalKey<FormState>();
final secondFormKey = GlobalKey<FormState>();
错误五:Key 放在太内层
Padding(
child: UserCard(
key: ValueKey(user.id),
),
)
问题表现
外层列表仍然按无 Key 的 Padding 匹配,State 可能无法跟随数据移动。
正确写法
Padding(
key: ValueKey(user.id),
child: UserCard(),
)
错误六:使用可变化字段作为 Key
ValueKey(user.name)
问题表现
用户修改名字后,Flutter将组件识别为新组件,旧 State 被销毁。
正确写法
ValueKey(user.id)
二十、Key 类型完整对比表
| Key 类型 | 唯一性判断方式 | 生命周期 | 典型场景 | 优点 | 缺点与注意事项 |
|---|---|---|---|---|---|
Key('value') | 字符串值的 == | 跟随 Widget 配置 | 简单字符串标识 | 写法简洁 | 实际是 ValueKey<String> |
ValueKey<T> | value 的 == 和 hashCode | 由传入值决定 | 用户 ID、消息 ID、列表排序、动画 | 最常用、语义清楚 | value 必须稳定且唯一 |
ObjectKey | 对象实例的 identical | 跟随对象实例 | 对象实例本身代表身份 | 不依赖对象字段 | API 重建对象后 Key 会变化 |
UniqueKey | 只与自身相等 | 由 Key 实例决定 | 无业务 ID、强制重置 State | 保证唯一 | 不能在每次 build 中创建 |
GlobalKey | Key 实例全局唯一 | 应由 State 长期持有 | FormState、获取 State/Context、测量组件 | 功能强大 | 匹配成本高,不能重复,不能频繁创建 |
GlobalObjectKey | 对象实例的 identical,全局范围 | 跟随对象和组件树 | 根据对象全局定位 State | 可重新创建同等 Key 找到同一对象 | 容易出现全局冲突,通常应使用私有子类 |
PageStorageKey | value 的 ==,结合祖先 Key 链 | 最近的 PageStorage 生命周期 | 保存列表滚动位置、Tab 页面状态 | 简单解决页面状态恢复 | 不是磁盘持久化,value 必须稳定 |
LabeledGlobalKey | GlobalKey 实例身份 | 应长期持有 | 带调试名称的 GlobalKey | 日志和调试更清晰 | debugLabel 不参与身份判断 |
二十一、实际项目中的选择原则
大多数业务可以按照以下顺序选择:
1. 不涉及列表增加、删除、排序或状态保留
=> 通常不需要 Key
2. 有稳定业务 ID
=> ValueKey(id)
3. 对象实例本身就是身份
=> ObjectKey(object)
4. 没有业务 ID,但需要一次性唯一身份
=> 创建一次 UniqueKey 并保存
5. 需要恢复滚动位置
=> PageStorageKey
6. 需要访问 FormState、State、Context 或 RenderObject
=> GlobalKey
7. 需要通过对象全局定位组件
=> GlobalObjectKey
8. 只是为了让 GlobalKey 更容易调试
=> GlobalKey(debugLabel: 'name')
最重要的原则是:
能使用 ValueKey,就不要使用 GlobalKey。
以及:
Key 必须代表数据或组件的稳定身份,而不是它当前所在的位置。