尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
谈谈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值相等 ()保存/恢复滚动位置、轻量状态持久化在需要状态持久化的特定滚动组件中使用。
RELATED

相关推荐

Go实战:实时推送系统设计

Go实战:实时推送系统设计

TL;DR 核心要点速览 Go适合后端API、微服务、云原生、CLI工具 高并发秒杀用Redis原子操作+限流 分布式锁用Redis SETNX或etcd租约 分布式事务用Saga模式或事件溯源 Go微服务通信用gRPC或消息队列 本篇是Go项目实战模块,含完整可运行项目 摘要:本文详细介绍实时推送系统设计,涵…

📅 2026/10/7 8:07:59
基于ResNet和脑部MRI影像的肿瘤早筛研究机器学习选题方向大数据毕设项目

基于ResNet和脑部MRI影像的肿瘤早筛研究机器学习选题方向大数据毕设项目

脑部肿瘤的早期筛查对于提高患者生存率和治疗效果具有重要意义。然而,传统的筛查方法存在效率低、准确性不高等问题。为此,本研究旨在利用深度学习技术,基于脑部MRI影像,开发一种高效的肿瘤早期筛查方法。本研究的主要内容包括脑部…

📅 2026/10/13 18:55:46
基于springboot的云南中草药知识普及管理系统网站源码+文档

基于springboot的云南中草药知识普及管理系统网站源码+文档

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

📅 2026/10/3 10:17:50
MORE NEWS

更多资讯

📰

蚁群算法做物流配送路径优化:原理、代码与调优实战

简介:这份资源是面向物流工程、运筹优化方向的学生与开发者的一套完整项目资料,围绕蚁群算法解决配送路径选择问题展开,适合课程设计、毕业设计或算法入门实践。压缩包共2000个文件,约60.96MB,以1753个js前端脚本、111…

📰

Delphi 12.3下TMS AI Studio:解决桌面AI流式响应与界面卡顿的控件方案

简介:Delphi 12.3下的人工智能组件库TMS AI Studio v1.2.0.0,面向需要在桌面应用中集成AI能力的Delphi开发者。它提供语音识别、图像处理、自然语言处理等控件,让开发者无需深入底层算法即可快速构建智能交互,同时保留充足的定制选…

📰

Dynamo节点包实战:从安装配置到团队标准件改造

简介:面向Revit与Dynamo使用者的离线节点包合集,主要解决官方包管理器联网不稳定、部分第三方节点包无法在线获取的问题,适合已具备一定可视化编程基础、希望扩展参数化设计能力的BIM工程师与设计人员。压缩包共约2000个文件、63.86MB&#x…

📰

Delphi 13.1安装KonopkaControls教程:从配置到避坑

简介:专为Delphi 13.1打造的KonopkaControls控件库(版本8.0.2),面向需要高效构建美观桌面应用界面的Delphi开发者,也适合希望深入定制UI组件的进阶用户。压缩包共2000个文件,大小37.58MB,文件类…

📰

Django招聘系统实战:爬虫+搜索+PostgreSQL闭环

简介:这是一套基于Django框架开发的云招聘系统实战项目资源,面向Python Web开发初学者与求职类应用实践者,解决招聘信息采集、存储、可视化展示与Web服务搭建的一站式学习需求。资源包共2000个文件,主体为362个JavaScript前端交互…

📰

开源本地语音工作室:可编排的声音克隆工作流引擎

1. 项目概述:这不是又一个“语音克隆玩具”,而是一套可嵌入、可编排、可交付的本地化语音生产基础设施 “开源声音克隆与工作流引擎,完全本地运行的开源语音工作室”——这个标题里没有一个词是虚的。我从去年底开始深度参与三个不同规模的语…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬