Key 机制:LocalKey、GlobalKey 与列表性能
Key 解决什么问题?
Flutter 的 Element 复用依赖「对比新旧 Widget 是否同类」。默认情况下,按树中位置比较。当列表项顺序变化时,位置不变而内容变,就会错乱。
// 问题场景:有状态的列表项乱序
Column(
children: showFirst
? [ColoredBox(color: Colors.red), ColoredBox(color: Colors.blue)]
: [ColoredBox(color: Colors.blue), ColoredBox(color: Colors.red)],
)
// 切换 showFirst 时,Element 不重建(位置没变),状态保留在原位置
// 如果 ColoredBox 有内部状态(如 checkbox 选中),会"串位"
Key 告诉 Flutter:用 Key 来匹配 Widget 和 Element,而不是用位置。
Key 的类型体系
Key (abstract)
├── LocalKey (abstract) — 同一父节点下唯一
│ ├── ValueKey<T> — 按值判等
│ ├── ObjectKey — 按对象引用判等
│ └── UniqueKey — 永远不等(强制重建)
└── GlobalKey (abstract) — 全树唯一
├── GlobalKey<T extends State>
└── GlobalObjectKey
LocalKey
ValueKey
最常用。用一个值(通常是业务 ID)来标识 Widget:
// 列表:用 item.id 作为 key
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
key: ValueKey(item.id), // 按 id 匹配 Element
title: Text(item.title),
);
},
)
// 当列表重排序时,Flutter 按 id 找到对应 Element,状态正确保留
UniqueKey
每次 build 都不同,强制销毁旧 Element 并新建:
// 场景:强制重建一个 Widget(如重置动画、刷新状态)
Widget build(BuildContext context) {
return SomeWidget(
key: _forceRebuildKey, // 赋值新 UniqueKey() 触发完全重建
);
}
void _reset() {
setState(() {
_forceRebuildKey = UniqueKey();
});
}
ObjectKey
按对象引用判等,适合数据对象没有直接 ID 的场景:
ObjectKey(someObject) // 等价于 ValueKey(identityHashCode(someObject))
GlobalKey
GlobalKey 让 State 在树中任意位置移动时保持存活,且允许从外部访问:
final _formKey = GlobalKey<FormState>();
// 在树中任意位置放置
Form(
key: _formKey,
child: ...,
)
// 从外部访问 State
void onSubmit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
}
GlobalKey 的代价
GlobalKey 有注册表,全树范围内唯一,创建和销毁都有性能开销:
- 不要在
build方法里创建 GlobalKey(每次 rebuild 会销毁旧的创建新的) - 应在 State 的
initState或作为 State 字段创建 - 能用 LocalKey 解决的问题不要用 GlobalKey
// ❌ 错误:每次 rebuild 都重建 GlobalKey
Widget build(BuildContext context) {
return Form(key: GlobalKey<FormState>(), ...); // 每次都新的 key,Form 不停重建
}
// ✅ 正确:在 State 字段中声明
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
// ...
}
可以为 Widget 添加 Key 的最佳位置
Rule of Thumb:在会改变同类型 Widget 顺序的列表/容器中,给有状态的子 Widget 加 Key。
无状态(StatelessWidget)的子节点通常不需要 Key,Flutter 无论如何都会根据新 Widget 的配置更新渲染,状态不会「漂移」。
// 有状态的卡片(内有 checkbox、输入框等)打乱顺序时需要 Key
AnimatedList(
itemBuilder: (context, index, animation) {
return SizeTransition(
sizeFactor: animation,
child: TodoCard(
key: ValueKey(todos[index].id), // ← 必须
todo: todos[index],
),
);
},
)
Key 与列表性能
在大列表中合理使用 Key 能避免大量不必要的 Widget 重建。考虑一个场景:排序后列表项全部平移:
排序前: [A, B, C, D]
排序后: [D, C, B, A]
不加 Key:Flutter 认为位置 0 还是 A,用新数据 D 更新 → 四个都重建
加 Key:Flutter 识别 D 移到了位置 0,仅移动 Element,无重建
实测在复杂 Item(如带图片、动画)的列表中,加 Key 可以显著减少帧时间。
核心架构问题
Q:Key 在 StatelessWidget 上有用吗?
有用,但不像 StatefulWidget 那样明显。StatelessWidget 以 Key 匹配时,Element 复用会跳过不必要的 build 调用。对于计算量大的 StatelessWidget,LocalKey 可以减少重建次数。
Q:热重载(Hot Reload)能保留有 GlobalKey 的 Widget 的状态吗?
可以。GlobalKey 注册表在热重载时不会清空,State 存活。这也是 GlobalKey 在调试时的一个隐性好处。