详解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),
   );
 }
}

详解Flutter中key的正确使用方式

点击删除按钮,从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'),
 ];

详解Flutter中key的正确使用方式

点击删除按钮,可以看到颜色错乱的现象消失了,一切正常。那么有没有想过,为什么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

标签:Flutter,key,使用方式
0
投稿

猜你喜欢

  • Android Intent实现页面跳转的方法示例

    2021-11-08 05:27:22
  • SpringBoot实现过滤器、拦截器与切片的实现和区别

    2023-04-28 22:51:04
  • Jaxb2实现JavaBean与xml互转的方法详解

    2023-11-25 11:51:38
  • C# ComboBox的联动操作(三层架构)

    2022-06-21 16:37:56
  • 利用C#9.0新语法如何提升if语句美感

    2021-06-15 12:48:54
  • 简单实现Android读取网络图片到本地

    2021-12-20 04:31:05
  • Java源码解析之ClassLoader

    2022-06-14 06:07:47
  • Java Switch对各类型支持实现原理

    2023-10-28 04:45:37
  • JWT.net 操作实践方法

    2022-04-09 16:01:28
  • springboot集成spring cache缓存示例代码

    2021-10-20 07:57:54
  • 基于Idea+Jconsole实现线程监控步骤

    2021-07-29 10:39:40
  • 2022 最新 IntelliJ IDEA 详细配置步骤演示(推荐)

    2021-11-20 21:05:48
  • Java使用Hutool实现AES、DES加密解密的方法

    2021-06-03 16:49:57
  • springboot+vue实现登录功能的最新方法整理

    2022-08-31 21:40:23
  • Android搜索框组件SearchView的基本使用方法

    2023-01-01 15:37:51
  • C#中的图像Image类与打印Printing类用法

    2022-07-25 06:24:36
  • MyBatis实现物理分页的实例

    2023-03-13 04:21:45
  • 一文教会你使用jmap和MAT进行堆内存溢出分析

    2023-11-06 08:01:57
  • C#使用SQL Dataset数据集代码实例

    2023-02-24 08:23:07
  • Spring refresh()源码解析

    2022-09-04 18:45:52
  • asp之家 软件编程 m.aspxhome.com