目录
前言
一、什么是Key
1.StatelessWidget
2.StatefulWidget
3.加入Key后的效果
二、什么时候应该使用 Key?
1.Flutter判断widget的逻辑
1.Flutter判断组件身份的规则
1.Widget的类型(runtimeType)相同
2. Key相同(key == key)
2.举例说明
3.Flutter在源码中的行为(简化版)
2.Flutter中key的使用场景
三、Key的几种类型
1.ValueKey
2.ObjectKey
3.UniqueKey
4.GlobalKey
四、使用建议
前言
在 Flutter 中,Key是一个非常核心但常常被初学者忽略的概念。理解 Key的工作机制,能够帮助你更深入地掌握 widget 的构建、重建与复用,尤其在构建动态列表、动画组件、状态保持等场景中尤为重要。
本文将通过两个典型示例(StatelessWidget与StatefulWidget),配合代码与动图讲解,让你真正理解:
什么是 Key?
它是怎么影响组件状态的?
什么时候该用?
用哪种 Key 更合适?
一、什么是Key
Key是widget的唯一标识符,用于告诉 Flutter:“这个 widget 在 widget tree 中是谁”。
在 Flutter 的渲染流程中,UI 是基于 widget tree 描述的。当你调用 setState() 或 UI 发生变化时,Flutter 会生成新的 widget tree,并与旧的 widget tree 进行比较(diff)来决定:
哪些 widget 可以复用
哪些 widget 需要销毁并重新创建
这个“比对”的核心依据,就是:runtimeType + Key。
如果两个 widget 的类型一样并且 Key 一样,则认为是“同一个” widget,Flutter 会复用它原有的 element 和 state
1.StatelessWidget
我们在页面上加载两个无状态的 Card 组件,点击浮动按钮时,交换它们的顺序,并使用 Provider 更新点击次数。
图1.无状态的widget
class CardWidget extends StatelessWidget {final String title;final Color color;final int count;final Function onClick;const CardWidget({super.key,required this.title,required this.color,required this.onClick,required this.count,});@overrideWidget build(BuildContext context) {return Card(color: color,child: ListTile(title: Text(title),subtitle: Text('我是 $title,点击次数:$count'),trailing: IconButton(icon: const Icon(Icons.add),onPressed: () => onClick(),),),);}
}
我们在 State 中用一个数组保存组件,并在点击按钮时交换顺序:
titles.insert(1, titles.removeAt(0)); // 交换两个 widget
运行后,我们会发现:
组件交换成功
点击次数保持正确
UI 没有异常
为什么?因为 StatelessWidget 没有内部状态,重建不会有状态丢失问题,Provider 中保存的状态仍然正确。
2.StatefulWidget
还是以上面的UI效果为例:我们使用StatefuleWidget来实现一下,这里代码就简单很多了:
Widget代码如下:
class StfCardWidget extends StatefulWidget {final String title;final Color color;const StfCardWidget({super.key, required this.title, required this.color});@overrideState<StfCardWidget> createState() => _StfCardWidgetState();
}class _StfCardWidgetState extends State<StfCardWidget> {int counter = 0;@overrideWidget build(BuildContext context) {return Card(color: widget.color,child: ListTile(title: Text(widget.title),subtitle: Text('我是 ${widget.title},点击次数:$counter'),trailing: IconButton(icon: const Icon(Icons.add),onPressed: () => setState(() => counter++),),),);}
}
调用上述Widget的实例代码如下:
class NoKeyDemo extends StatefulWidget {final String title;const NoKeyDemo({super.key, required this.title});@overrideState<NoKeyDemo> createState() => _NoKeyDemoState();
}class _NoKeyDemoState extends State<NoKeyDemo> {late List<Widget> titles;@overridevoid initState() {super.initState();titles = [StfCardWidget(title: 'CardA', color: Colors.red,),StfCardWidget(title: 'CardB', color: Colors.green,),];}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title)),body: Column(children:titles,),floatingActionButton: FloatingActionButton(onPressed: () {setState(() {titles.insert(1, titles.removeAt(0));});},child: const Icon(Icons.swap_vert),),);}
}
这个时候,我们会发现UI显示会出现异常:
图2.StatefulWidget交换
交换顺序后,你会看到:
❌ 组件颜色交换了
❌ 文本标题也交换了
⚠️ 点击次数却没有交换!
3.加入Key后的效果
我们只需要简单地为每个 widget 添加唯一 Key 即可修复问题:
titles = [StfCardWidget(title: 'CardA', color: Colors.red,key: const ValueKey("CardA"),),StfCardWidget(title: 'CardB', color: Colors.green,key: const ValueKey("CardB"),), ];
现在,Flutter 会根据 Key 精确判断哪个组件是 CardA、哪个是 CardB,并且能保留各自的状态。
✅ UI 状态正常
✅ 点击次数准确
✅ 没有错误重用
二、什么时候应该使用 Key?
1.Flutter判断widget的逻辑
在 Flutter 中,判断一个组件是“同一个组件”还是“新组件”的核心逻辑发生在 Widget → Element 的构建与更新阶段。
Flutter 在执行 setState() 或重新构建 widget tree 时,会尝试“复用”已有的元素结构(Element)。这个过程就需要判断新旧 widget 是否表示相同的界面组件,而这个判断过程依赖于以下几个关键因素:
1.Flutter判断组件身份的规则
Flutter 使用以下逻辑判断组件是否“是同一个”:
1.Widget的类型(runtimeType)相同
Flutter 首先比较新旧 widget 的类型是否相同:
oldWidget.runtimeType == newWidget.runtimeType
如果类型不同,必定是不同组件,Flutter 会销毁旧的 Element 并新建一个。
2. Key相同(key == key)
如果类型相同,Flutter 再比较 Key:
oldWidget.key == newWidget.key
如果Key相同:认为是“同一个组件”,复用对应的 Element 和 State;
如果Key不同或缺失:Flutte 默认按照在 widget 树中的顺序来“尝试匹配”;
这在组件结构发生变化或排序变动时会导致状态错乱或内容错位。
总结一句话:
Flutter会认为 “类型 + Key” 一致的 widget 是“同一个”,从而复用状态;否则就会创建新的 widget / element / state。
2.举例说明
✅ 正确复用(加了 Key)
Widget build(BuildContext context) {return Column(children: [MyWidget(key: ValueKey('A')),MyWidget(key: ValueKey('B')),],);
}
即使交换顺序,只要 Key 相同,状态不会错乱。
❌ 状态错乱(没加 Key)
Widget build(BuildContext context) {return Column(children: [MyWidget(), // 没 keyMyWidget(), // 没 key],);
}
交换顺序后,Flutter 只能按位置匹配,无法准确判断哪个 widget 是原来的哪个,从而导致 UI 状态错位。
3.Flutter在源码中的行为(简化版)
Flutter 在 Element.updateChild() 方法中大致是这样判断的(伪代码):
if (oldWidget.runtimeType == newWidget.runtimeType &&oldWidget.key == newWidget.key) {// 是同一个 widget,调用 updateelement.update(newWidget);
} else {// 是新 widget,销毁旧的 element,创建新的oldElement.deactivate();newElement = inflateWidget(newWidget);
}
你可以在 Flutter 的源码 widgets/framework.dart 中找到这部分逻辑。
2.Flutter中key的使用场景
这里作者总结了一些key的使用场景。
使用场景 | 是否建议使用 Key | 推荐 Key 类型 | 说明 |
---|---|---|---|
动态列表(ListView.builder) | ✅ 必须 | ValueKey | 区分每个列表项的身份 |
可排序列表(ReorderableListView) | ✅ 必须 | ValueKey | 不加 Key 无法正确交换 |
增删组件时状态保持 | ✅ | ValueKey 或 ObjectKey | 比如添加删除输入框时保留内容 |
交替显示两个相同类型组件(条件渲染) | ✅ | ValueKey | 用于避免状态重用错误 |
拖动动画 / Hero 动画 | ✅ | ValueKey | 保证动画关联组件正确匹配 |
强制组件重建(比如重载某个部分) | ✅ | UniqueKey | 每次都不同,不复用 |
跨树保留状态 | ✅ 谨慎使用 | GlobalKey | 用于 Tab、导航或表单场景,但性能开销较大 |
静态布局 / 简单组件 | ❌ 不需要 | - | 比如纯文本、无状态布局组件 |
在开发的过程中只要你遇到组件状态异常、数据错位、动画错位等问题,先看是不是忘了设置 Key!
三、Key的几种类型
1.ValueKey
最常用的Key类型,用于根据具体值判断是否是同一组件。
它的使用场景如下:
1.列表项有唯一标识(如 ID)
2.拖拽排序时标识组件
3.条件切换相似组件时防止错乱
示例代码如下:
ListView(children: items.map((item) => ListTile(key: ValueKey(item.id),title: Text(item.title),)).toList(),
);
2.ObjectKey
根据对象引用来判断是否为同一个组件。
它的使用场景如下:
1.数据模型实例(同值但不同引用)需要被唯一识别
2.对象不能简单地用一个字段表示唯一性
示例代码如下:
ObjectKey(userModel); // 只有当 userModel 是同一引用时才认为是同一组件
3.UniqueKey
每次都是新的,不可比较,用于强制刷新组件。
它的使用场景如下:
1.组件内部状态不可复用时强制刷新
2.临时widget(如动画组件、过渡组件)
示例代码如下:
MyWidget(key: UniqueKey()); // 每次都会新建 State
4.GlobalKey
它的使用场景如下:
1.跨组件调用组件的方法(如 Form 的验证、滚动)
2.状态跨布局保留(如 Tab 页、动画合并)
3.控制多个组件状态(慎用)
示例代码如下:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();Form(key: _formKey,child: ...
);// 在其他地方调用
_formKey.currentState?.validate();
⚠️ 警告:
-
GlobalKey 会强制整个 subtree rebuild,性能开销大;
-
Flutter 官方建议谨慎使用,仅用于必要场景。
四、使用建议
在实际的开发过程中,我总结了一下建议:
✅ 大多数场景用 ValueKey 就够了,如列表、条件渲染、动画组件。
🚫 不要滥用 GlobalKey,它会导致性能问题,限制复用优化。
🔁 如果你发现组件状态错乱,第一件事就是检查是否缺失 Key。