详解Flutter中key的正确使用方式
作者:半点橘色 发布时间:2021-11-05 04:31:02
1、什么是key
Widget中有个可选属性key,顾名思义,它是组件的标识符,当设置了key,组件更新时会根据新老组件的key是否相等来进行更新,可以提高更新效率。但一般我们不会去设置它,除非对某些具备状态且相同的组件进行添加、移除、或者排序时,就需要使用到key,不然就会出现一些莫名奇妙的问题。
例如下面的demo:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: Scaffold(
appBar: AppBar(
title: const Text('key demo'),
),
body: const KeyDemo(),
),
);
}
}
class KeyDemo extends StatefulWidget {
const KeyDemo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _KeyDemo();
}
class _KeyDemo extends State<KeyDemo> {
final List<ColorBlock> _list = [
const ColorBlock(text: '1'),
const ColorBlock(text: '2'),
const ColorBlock(text: '3'),
const ColorBlock(text: '4'),
const ColorBlock(text: '5'),
];
@override
Widget build(BuildContext context) {
return Column(
children: [
..._list,
ElevatedButton(
onPressed: () {
_list.removeAt(0);
setState(() {});
},
child: const Text('删除'),
)
],
);
}
}
class ColorBlock extends StatefulWidget {
final String text;
const ColorBlock({Key? key, required this.text}) : super(key: key);
@override
State<StatefulWidget> createState() => _ColorBlock();
}
class _ColorBlock extends State<ColorBlock> {
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 50,
color: color,
child: Text(widget.text),
);
}
}
点击删除按钮,从ColorBlock的列表中删除第一个元素,可以观察到颜色发生了错乱,删除了1号色块,它的颜色状态转移到了2号身上。这种情况在实际开发中往往会造成不小的麻烦。
这时,就需要为每个ColorBlock设置key值,来避免这个问题。
final List<ColorBlock> _list = [
const ColorBlock(key: ValueKey('1'), text: '1'),
const ColorBlock(key: ValueKey('2'), text: '2'),
const ColorBlock(key: ValueKey('3'), text: '3'),
const ColorBlock(key: ValueKey('4'), text: '4'),
const ColorBlock(key: ValueKey('5'), text: '5'),
];
点击删除按钮,可以看到颜色错乱的现象消失了,一切正常。那么有没有想过,为什么ColorBlock有key和没key会出现这种差异?
2、key的更新原理
我们来简单分析下key的更新原理。
首先,我们知道Widget是组件配置信息的描述,而Element才是Widget的真正实现,负责组件的布局和渲染工作。在创建Widget时会对应的创建Element,Element保存着Widget的信息。
当我们更新组件时(通常指调用setState方法)会遍历组件树,对组件进行新旧配置的对比,如果同个组件信息不一致,则进行更新操作,反之则不作任何操作。
/// Element
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
final Element newChild;
/// 更新逻辑走这里
if (child != null) {
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
/// 判断新旧组件为同一个组件则进行更新操作
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
if (!kReleaseMode && debugProfileBuildsEnabled)
Timeline.finishSync();
}
} else {
/// 创建逻辑走这里
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
通过Element中的updateChild进行组件的更新操作,其中Widget.canUpdate是判断组件是否需要更新的核心。
/// Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
canUpdate的代码很简单,就是对比新老组件的runtimeType和key是否一致,一致刚表示为同一个组件需要更新。
结合demo,当删除操作时,列表中第一个的组件oldWidget为ColorBlock(text: '1'),newWidget为ColorBlock(text: '2') ,因为我们将text和color属性都存储在State中,所以 oldWidget.runtimeType == newWidget.runtimeType为true,oldWidget.key == newWidget.key 为null,也等于true。
于是调用udpate进行更新
/// Element
void update(covariant Widget newWidget) {
_widget = newWidget;
}
可以看出,update也只是简单的更新Element对Widget的引用。 最终新的widget更新为ColorBlock(text: '2'),State依旧是ColorBlock(text: '1')的State,内部的状态保持不变。
如果添加了Key,刚oldWidget.key == newWidget.key为false,不会走update流程,也就不存在这个问题。
3、key的分类
key有两个子类GlobalKey和LocalKey。
GlobalKey
GlobalKey全局唯一key,每次build的时候都不会重建,可以长期保持组件的状态,一般用来进行跨组件访问Widget的状态。
class GlobalKeyDemo extends StatefulWidget {
const GlobalKeyDemo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _GlobalKeyDemo();
}
class _GlobalKeyDemo extends State<GlobalKeyDemo> {
GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Column(
children: [
ColorBlock(
key: _globalKey,
),
ElevatedButton(
onPressed: () {
/// 通过GlobalKey可以访问组件ColorBlock的内部
(_globalKey.currentState as _ColorBlock).setColor();
setState(() {});
},
child: const Text('更新为红色'),
)
],
);
}
}
class ColorBlock extends StatefulWidget {
const ColorBlock({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ColorBlock();
}
class _ColorBlock extends State<ColorBlock> {
Color color = Colors.blue;
setColor() {
color = Colors.red;
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 50,
color: color,
);
}
}
将组件的key设置为GlobalKey,可以通过实例访问组件的内部属性和方法。达到跨组件操作的目的。
LocalKey
LocalKey局部key,可以保持当前组件内的子组件状态,用法跟GlobalKey类似,可以访问组件内部的数据。
LocalKey有3个子类ValueKey、ObjectKey、UniqueKey。
ValueKey
可以使用任何值做为key,比较的是两个值之间是否相等于。
class ValueKey<T> extends LocalKey {
const ValueKey(this.value);
final T value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ValueKey<T>
&& other.value == value;
}
/// ...
}
ObjectKey:
可以使用Object对象作为Key,比较的是两个对象内存地址是否相同,也就是说两个对象是否来自同一个类的引用。
class ObjectKey extends LocalKey {
const ObjectKey(this.value);
final Object? value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
/// identical函数: 检查两个引用是否指向同一对象
return other is ObjectKey
&& identical(other.value, value);
}
/// ...
}
UniqueKey
独一无二的key,Key的唯一性,一旦使用UniqueKey,那么将不存在element复用
class UniqueKey extends LocalKey {
UniqueKey();
@override
String toString() => '[#${shortHash(this)}]';
}
来源:https://juejin.cn/post/7189816647891288123


猜你喜欢
- 本文实例为大家分享了C#实现简单的计算器功能的具体代码,供大家参考,具体内容如下环境:VS2010及以上版本1、建立个Window窗体应用2
- 使用抽象类应该注意的几个要点:包含一个或者多个抽象方法的类必须被声明为抽象类. 将类声明为抽象类,不一定含有抽象方法.通常认为,在抽象类中不
- 前言今天我们来讨论一下,程序中的错误处理。在任何一个稳定的程序中,都会有大量的代码在处理错误,有一些业务错误,我们可以通过主动检查判断来规避
- 1.配置多个数据源多个数据源是指在同一个系统中,用户数据来自不同的表,在认证时,如果第一张表没有查找到用户,那就去第二张表中査询,依次类推。
- 迷宫项目实现设计文档项目介绍:一个网格迷宫由n行m列的单元格组成,每个大院个要么是空地(用0表示),要么是障碍物(用1表示)。你的任务是找一
- 在日常的app使用中,我们会在android 的app中看见 热门标签等自动换行的流式布局,今天,我们就来看看如何自定义一个类似热门标签那样
- 本文实例为大家分享了Java实现窗体程序显示日历的具体代码,供大家参考,具体内容如下实训要求:1.使用BorderLayout 进行总体布局
- IEnumerable这个接口在MSDN上是这么说的,它是一个公开枚举数,该枚举数支持在非泛型集合上进行简单的迭代。换句话说,对于所有数组的
- 这篇文章主要介绍了springboot 定时任务@Scheduled实现解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的
- Reflections通过扫描classpath,索引元数据,并且允许在运行时查询这些元数据。使用Reflections可以很轻松的获取以下
- 前言最近接手了一个老项目,“愉悦的心情”自然无以言表,做开发的朋友都懂,这里就不多说了,都是泪...
- 本文实例为大家分享了C#通过NPOI导入导出数据EXCEL的具体代码,供大家参考,具体内容如下其实从数据库到服务器导入导出有很多方法,但是比
- javabean与map的转换有很多种方式,比如:1、通过ObjectMapper先将bean转换为json,再将json转换为map,但是
- 本文实例讲述了Android实现点击AlertDialog上按钮时不关闭对话框的方法。分享给大家供大家参考。具体如下:开发过程中,有时候会有
- 本文实例讲述了java实现的简单猜数字游戏代码。分享给大家供大家参考。具体代码如下:import java.util.InputMismat
- 因重定向无法正常goBack()解决方案首先说下问题,初始页面为A,点击某个链接跳转到B(http://xxx.com.cn/),B页面重定
- 1. JVM 运行时数据区JVM运行时数据区可以分为元空间,堆,虚拟机栈,本地方法栈,程序计数器五大块。元空间(方法区):存放类模版对象,是
- 前言很久没写BLOG了,之前在写Android聊天室的时候答应过要写一个客户(好友)之间的聊天demo,Android 基于Socket的聊
- 前几天,收到 AS 发布的 3.0 更新,就迫不及待的更新了,更新后发现整个界面的画风都变了,和 IDEA 更像了本人是命令行重度使用患者,
- 在实际的工作中直接使用反射的机会比较少,有印象的就是一次自己做的WinForms小工具的时候利用反射来动态获取窗体上的每个控件,并且为必要的