谈谈Flutter中的Key(二)——常用的Key

谈谈Flutter中的Key(二)——常用的Key 前言在上一篇文章中我们探讨了 Flutter 中 Key 的基本概念、作用以及为什么需要 Key。本文将继续深入重点介绍 Flutter 开发中几种常用且重要的 Key 类型包括它们的适用场景、使用方式以及背后的原理。1.Key的继承体系Key (抽象类)├── LocalKey (抽象类)│ ├── ValueKeyT│ ├── ObjectKey│ └── UniqueKey└── GlobalKeyT extends State├── GlobalObjectKey└── LabeledGlobalKey1.LocalKey--同一父组件内的身份标识1. ValueKeyValueKey是最常用、最基础的 Key 类型之一。它通过一个值通常是简单数据类型如int、String、double或自定义对象的hashCode来唯一标识一个 Widget。我们可以看一下ValueKey的定义ValueKey 用一个[值]来唯一标识一个Widget。两个ValueKey只要value相等就认为是同一个 key。判断等价的源码逻辑:overridebool operator (Object other) {return other is ValueKeyT other.value value;}就是这么简单--我们使用判断value是否相同class ValueKeyT extends LocalKey { /// Creates a key that delegates its [operator] to the given value. const ValueKey(this.value); /// The value to which this key delegates its [operator]. final T value; override bool operator (Object other) { if (other.runtimeType ! runtimeType) { return false; } return other is ValueKeyT other.value value; } override int get hashCode Object.hash(runtimeType, value); override String toString() { final valueString T String ? $value : $value; if (runtimeType _TypeOfValueKeyT) { return [$valueString]; } return [$T $valueString]; } }1.1 基本用法我们先用一个简单的例子看一下不使用ValueKey会发生的例子。示例Demo如下所示图1.两个计时器的例子我们两个除了颜色有区别其余属性都相同的Widget把按钮的两个属性值保存在一个数组中。这两个按钮都有一个手势点击之后计数器1当我们点击切换顺序按钮之后修改下两个按钮的下标值思考下计数器会跟随颜色变化么点击之后我们会发现颜色切换过去了但是计数的值没有跟随颜色一起变不知道你猜对了没有。切换后的UI如下图2.图2.切换色块背景颜色我们分析下原因首先我们知道Flutter直接复用旧State1.新旧Widget类型完全一致(runtimType相同)2.新旧Widget的Key完全相同。伪代码底层逻辑如下if (oldWidget.runtimeType newWidget.runtimeType oldWidget.key newWidget.key) {// 复用旧 State不销毁、不重建return oldState;}// 否则销毁旧 State创建新 State在上述的例子中我们没有使用ValueKey这个时候keynull,系统默认两个组件相同所以点击切换按钮的时候直接复用了State,这就导致了UI换了、状态残留--错位。那么既然找到了原因解决的方案也很简单给不同的Widget设置不同的ValueKey即可。完整代码如下import package:flutter/material.dart; class ValueKeyDemoPage extends StatefulWidget { final String title; const ValueKeyDemoPage({super.key, required this.title}); override StateValueKeyDemoPage createState() _ValueKeyDemoPageState(); } // 极简数据模型 class _BlockData { final String name; final Color color; _BlockData({required this.name, required this.color}); } class _ValueKeyDemoPageState extends StateValueKeyDemoPage { final List_BlockData _blocks [ _BlockData(name: 红色, color: Colors.red), _BlockData(name: 蓝色, color: Colors.blue), ]; Listint _order const [0, 1]; // 切换区块顺序 void _swap() setState(() _order _order.reversed.toList()); override Widget build(BuildContext context) { final orderedList [for (final i in _order) _blocks[i]]; return Scaffold( appBar: AppBar(title: Text(widget.title)), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // 无Key状态错位 const Text(无Key状态错位), const SizedBox(height: 12,), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: orderedList.map((e) _CountBlock(item: e)).toList(), ), const SizedBox(height: 30), // 有ValueKey状态正常跟随 const Text(有ValueKey状态跟随数据), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: orderedList.map((e) _CountBlock(key: ValueKey(e.name), item: e)).toList(), ), const SizedBox(height: 30), FilledButton(onPressed: _swap, child: const Text(切换顺序)) ], ), ), ); } } // 极简计数色块组件 class _CountBlock extends StatefulWidget { final _BlockData item; const _CountBlock({super.key, required this.item}); override State_CountBlock createState() _CountBlockState(); } class _CountBlockState extends State_CountBlock { int _count 0; override Widget build(BuildContext context) { return GestureDetector( onTap: () setState(() _count), child: Container( width: 140, height: 140, decoration: BoxDecoration( color: widget.item.color, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.item.name, style: const TextStyle(color: Colors.white, fontSize: 16)), Text(计数: $_count, style: const TextStyle(color: Colors.white)) ], ), ), ); } }1.2 适用场景列表项的重排、增删当ListView、GridView或Column/Row中的子 Widget 顺序可能发生变化时为每个子项设置ValueKey可以帮助 Flutter 高效地更新 UI而不是重建所有 Widget。表单字段的保持状态例如一个包含多个TextField的表单当动态增删字段时为每个TextField设置ValueKey可以保持其输入状态文本、焦点等。条件渲染中的 Widget 标识当根据条件显示不同的 Widget 时使用不同的ValueKey可以告诉 Flutter 这是两个不同的 Widget 实例从而触发正确的生命周期如initState。1.3 原理浅析ValueKey的相等性比较基于其内部的value属性。两个ValueKey相等当且仅当它们的value相等使用操作符比较。这使得它非常轻量且高效。2. ObjectKeyObjectKey 与ValueKey类似但它使用对象引用Object本身作为标识而不是对象的值。这意味着它比较的是对象的身份identity而不是内容。2.1 基本用法class User { final String id; final String name; User(this.id, this.name); } // 在 Widget 树中使用 final userA User(001, Alice); final userB User(001, Alice); // 内容相同但是不同的对象实例 Widget build(BuildContext context) { return Column( children: [ UserProfile(user: userA, key: ObjectKey(userA)), UserProfile(user: userB, key: ObjectKey(userB)), // 即使内容相同Key也不同 ], ); }2.2 适用场景可变对象集合当你的数据模型是可变对象并且可能在内存中被替换为新的实例但逻辑上代表同一个实体时使用ObjectKey可以确保 Widget 与特定的对象实例绑定。需要区分同一值的不同实例有时两个对象的值可能相等但它们是不同的实例你希望 Flutter 将它们视为不同的 Widget。这时ObjectKey比ValueKey更合适。与ValueKey互补当对象的操作符被重写但你仍然想用对象身份来标识 Widget 时。2.3 与 ValueKey 的区别核心区别在于相等性判断的依据ValueKey基于value other.value。ObjectKey基于identical(value, other.value)即是否是同一个对象。因此对于不可变且值唯一的数据如从数据库读取的 IDValueKey是首选。对于可变对象或在内存中可能被新实例替换的对象ObjectKey更可靠。3. UniqueKeyUniqueKey会在每次创建时生成一个唯一的标识。它不依赖于任何外部值因此每个UniqueKey实例都是不同的。3.1 基本用法// 每次 build 都会生成一个新的 UniqueKey AnimatedSwitcher( duration: Duration(milliseconds: 300), child: Text( Hello, key: UniqueKey(), // 关键在这里 ), ); // 或者用于需要强制重建的 Widget bool showAdvanced false; Widget build(BuildContext context) { return Column( children: [ if (showAdvanced) AdvancedSettings(key: UniqueKey()), // 每次显示都会是全新的实例 // ... 其他部件 ], ); }3.2 适用场景强制 Widget 完全重建当你希望一个 Widget 在每次显示时都从头开始初始化其状态State时为其提供一个UniqueKey。常见于AnimatedSwitcher的子Widget或者动态显示/隐藏的复杂表单部分。临时性或一次性的WidgetWidget 不需要在树移动中保持身份或者其身份本身就不重要。没有稳定标识符时作为最后的手段当无法为 Widget 提供一个稳定的ValueKey或ObjectKey时。3.3 注意事项因为每次build都会创建新的UniqueKey这会导致 Flutter 认为旧的 Widget 被移除新的 Widget 被添加从而触发不必要的 Widget 重建和状态丢失。这可能会损害性能。仅在明确需要强制刷新状态时才使用它。2.GlobalKey —— 全局唯一标识1. GlobalKeyGlobalKey是Key家族中最特殊也最强大的一员。它不仅在 Widget 树内唯一标识一个 Widget,还允许你从树的任何地方访问该 Widget 的State或关联的Element、BuildContext。4.1 基本用法// 定义一个 GlobalKey final GlobalKeyFormState _formKey GlobalKeyFormState(); final GlobalKeyScaffoldState _scaffoldKey GlobalKeyScaffoldState(); final GlobalKey_MyWidgetState _myWidgetKey GlobalKey_MyWidgetState(); Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, body: Form( key: _formKey, child: Column( children: [ MyWidget(key: _myWidgetKey), ElevatedButton( onPressed: () { // 使用 GlobalKey 访问状态并调用方法 if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); _myWidgetKey.currentState?.doSomething(); _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(Success!))); } }, child: Text(Submit), ), ], ), ), ); }4.2 适用场景表单验证与提交Form和FormField的经典搭配。跨组件访问状态当一个 Widget 需要调用另一个“遥远”的 Widget 的状态方法时应优先考虑状态管理方案如 Provider、Riverpod 等。获取 Widget 的尺寸或位置通过GlobalKey获取BuildContext然后使用RenderBox进行布局计算。导航或对话框的全局控制例如在没有BuildContext的地方如业务逻辑层触发 SnackBar 或 Dialog。4.3 注意事项与代价性能开销GlobalKey需要在整个 App 生命周期中维护一个全局映射比LocalKeyValueKey、ObjectKey、UniqueKey开销大。破坏封装性它允许从外部直接操作 Widget 的内部状态违背了 Flutter 的响应式设计原则可能导致难以维护的代码。谨慎使用仅在确实需要跨组件边界访问状态且其他状态管理方案不适用时才考虑使用。2. PageStorageKeyPageStorageKey是一种特殊的ValueKey专用于与PageStorage组件协作用于在 Widget 树变化如路由切换、列表滚动时保存和恢复滚动位置或其他状态。5.1 基本用法// 在 ListView 或 GridView 中使用 ListView.builder( key: PageStorageKeyString(my_list_view), // 为此列表提供一个持久化键 itemCount: 100, itemBuilder: (context, index) ListTile(title: Text(Item $index)), ); // 在 TabBarView 中每个 Tab 的页面通常使用 PageStorageKey 来保持滚动位置 TabBarView( children: [ ListView.builder( key: PageStorageKey(tab1), // ... ), ListView.builder( key: PageStorageKey(tab2), // ... ), ], );5.2 工作原理Flutter 提供了一个名为PageStorage的InheritedWidget它可以为子树提供一个“存储桶”。当带有PageStorageKey的 Widget 从树中移除时其关联的状态如ScrollController的滚动偏移量会被保存到PageStorage中。当该 Widget 再次被插入树中时并且使用相同的PageStorageKey状态会被自动恢复。5.3 适用场景保持滚动位置在TabBarView、NavigationRail、BottomNavigationBar切换页面时保持每个页面内ListView、GridView、CustomScrollView的滚动位置。保存表单状态在页面暂时不可见时保存表单字段的输入内容虽然更推荐使用状态管理。任何需要跨 Widget 生命周期保存的轻量状态。总结Key 类型父类标识依据主要用途使用建议ValueKeyLocalKey值相等 ()列表项、表单字段、有稳定ID的Widget首选用于大多数需要稳定标识的场景。ObjectKeyLocalKey对象相同 (identical)可变对象集合、需区分同一值的不同实例当ValueKey因对象可变性不适用时使用。UniqueKeyLocalKey每次创建都唯一强制重建、临时Widget、无稳定ID时谨慎使用避免不必要的性能损耗。GlobalKeyKey全局唯一可访问State表单、跨组件访问状态、获取布局信息不得已而用之优先考虑状态管理库。PageStorageKeyValueKey值相等 ()保存/恢复滚动位置、轻量状态持久化在需要状态持久化的特定滚动组件中使用。