likes
comments
collection
share

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

作者站长头像
站长
· 阅读数 7

1. 问题引入

🤡 写了一阵子UI,不知道大家有没有发现,在创建自定义Widget时,编译器总会贴心地提示我们 在构造方法中添加一个key

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

将自定义Widget的key参数传递给父类Widget的构造方法,如果初始化自定义Widget实例时没有设置key参数,那么这个属性值就会是 null。那这个key到底是拿来干嘛的呢?

答:在Widget树中 唯一标识(不能重复使用)比较Widget,以及在 Widget移动或改变时保持其状态。一般不需要设置它,除非是 对某些具备状态且相同的组件进行增删或排序

写个没设置Key引起BUG的经典例子 (点击移除Widget数组中第一个元素):

import 'package:flutter/material.dart';

class TestKeyPage extends StatefulWidget {
  const TestKeyPage({super.key});

  @override
  State<StatefulWidget> createState() => _TestKeyPageState();
}

class _TestKeyPageState extends State<TestKeyPage> {
  List<Widget> items = [
    const TestKeyWidget(color: Colors.green),
    const TestKeyWidget(color: Colors.blue),
    const TestKeyWidget(color: Colors.red)
  ];

  @override
  Widget build(BuildContext context) => Scaffold(
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.remove),
          onPressed: () {
            items.removeAt(0);	// 点击移除
            setState(() {});
          }),
      body: Column(
        children: items,
      ));
}

class TestKeyWidget extends StatefulWidget {
  final Color color;

  const TestKeyWidget({super.key, required this.color});

  @override
  State<StatefulWidget> createState() => _TestKeyWidgetState();
}

class _TestKeyWidgetState extends State<TestKeyWidget> {
  int count = 0;

  void increment() {
    setState(() {
      ++count;
    });
  }

  @override
  Widget build(BuildContext context) => Container(
      color: widget.color,
      width: 100,
      height: 100,
      alignment: Alignment.center,
      child: GestureDetector(
        onTap: increment,
        child: Text("$count", style: const TextStyle(color: Colors.white, fontSize: 30)),
      ));
}

运行后,点击数字自增,让三个Widget数字依次显示为:绿1蓝2红3,然后点击移除按钮:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

😳 第一个绿色Widget被移除了,但是数字显示不对,应该是:蓝2红3,现在却变成了:蓝1红2。解法也很简单,为每个TestKeyWidget指定一个key,如:

List<Widget> items = [
  const TestKeyWidget(key: ValueKey(1), color: Colors.green),
  const TestKeyWidget(key: ValueKey(2), color: Colors.blue),
  const TestKeyWidget(key: ValueKey(3), color: Colors.red)
];

此时再次重复上面的操作,Widget正确移除,数字也显示正确:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

没有Key来标识Widget,发生重排序后,Flutter会 错误地关联状态,视觉上就看到:位置移动 了,但是 状态却没随之移动,导致状态看起来 "丢失" 了。

😁 精简下就是

在需要保持状态 + 涉及到重排序的场景,不设置Key,都会有"状态丢失"的问题。

接着了解下Flutter中Kye相关的API~

2. Key详解

Key的类继承结构图如下:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

2.1. Key

抽象类,有一个工厂构造方法,用于创建一个 ValueKey,一般不直接使用,而是用它的两个子类 LocalKeyGlobalKey

2.2. GlobalKey

全局唯一Key每次build都不会重建可以长期保持组件的状态一般用于跨组件访问Widget的状态。使用代码示例如下:

// 定义一个GlobalKey
final GlobalKey _globalKey = GlobalKey();

// 获得BuildContext、State 以及 Widget
_globalKey.currentContext;
_globalKey.currentState;
_globalKey.currentWidget;

// 获得 State,调用其中的属性示例
final state = _globalKey.currentState as _TestWidgetState;
state.count++;
print(state.count);
state.setState(() {});

// 获得 Widget,调用其中的属性示例
final widget = _globalKey.currentWidget as TestWidget;
print(widget.color);	// 获得控件颜色

// 获得 Context,调用其中的属性示例
final renderBox = _globalKey.currentContext!.findRenderObject() as RenderBox;
print(renderBox.size);	// 获得控件尺寸
print(renderBox.localToGlobal(Offset.zero))	// 获得控件坐标

Tips:🤡 不要在build() 方法中创建GlobalKey!!!性能不好不说,还可能出现意想不到的异常,如:子树里的GestureDetector可能会由于每次build时重新创建GlobalKey而无法继续追踪手势事件。

接着简单看下源码:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

可以看到默认实现是 LabeledGlobalKey 类,也看下这个类的实现:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

内部就一个 debugLabel 属性,仅仅为了debug时使用,实际开发不会传递这个参数,然后重写了toString() 方法。好像也没啥亮点🤔?往回看 GlobalKey 的源码,可以看到BuildContext、Widget、State 其实都是通过 _currentElement 属性来获取的,跟下 _globalKeyRegistry,指向 BuildOwner类中的一个map:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

不难看出 key为GlobalKey对象value为与之关联的Element,接着分别看下是分别是啥时候 建立关联解除关联 的。搜下 _globalKeyRegistry[ 定位到了 _registerGlobalKey() 方法:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

继续跟,定位到 Element#mount() 调用了这个方法:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

新建的Widget添加到Widget树 时,Flutter会为它创建一个新的Element对象,并调用 mount() 方法将Element插入到Element树中,并关联一个新的或现有的渲染对象 (RenderObject),这个过程就是所谓的 挂载

与之对应的 卸载 则是通过 unmount() 方法实现,当Element被永久移除出渲染树时调用的,通常是与之关联的Widget在树中已经不存在或者被替换成了另一个不同类型的Widget。该方法主要执行一些清理操作,如:释放资源,解除监听器等。

反过来跟下 unmount() 方法,可以看到其中调用了 _unregisterGlobalKey()

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

跟下这个方法:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

果然,在这个方法里,根据key移除了对应的Element。 然后说下 GlobalKey 为什么是全局唯一的:

  • 调用 GlobalKey构造方法,默认返回一个 新建的 LabeledGlobalKey 对象
  • 该类中没有对 hashCode()equals() 方法进行重写,判断两个对象是否相等,直接通过 引用比较(是否指向内存中的相同对象) 得出结果,而且构造函数也没有用const修饰。

🤣 每次都是 创建新的对象作为Key,自然能保证 全局唯一 啊!接着,顺带提下另一个实现类 GlobalObjectKey,源码如下:

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

重写了 equals() 和 hashCode() 方法,内部维护一个Object对象,通过判断此对象是否指向同一块内存地址来判断两个GlobalObjectKey是否相等。

Tips: 🤡 源码里没看到equals(),只看到了 operator ==,这是 运算符重载 的写法,作用都是一样的,用于判断两个对象是否相等。重写了 equals()hashCode() 方法判断对象是否相等的流程:判断两者的哈希码是否相等,不等返回false,相等再执行equals() 进行下一步判断。identical() 用于检查两个引用是否指向同一对象。

所以,如果使用 GlobalObjectKey,是否能实现 全局唯一性 取决于你传入的Object对象是否是唯一的!

😶 另外,不要滥用 GlobalKey,比如下面两个场景:

  • 没必要保存控件的状态也设置GlobalKey,造成没必要的内存浪费;
  • ListView中为每个item都设置一个GlobalKey,任何条目改变时,Flutter都需要重新检查整个列表,当列表很长时,会导致严重的性能下降,由此导致不佳的用户体验。

2.3. LocalKey

局部唯一Key, 或者说是 同级唯一Key,在同一父级元素中必须是唯一的,一般用于 同级Widget间的比较和重排序。问题引入部分的代码示例就用到了 LocalKey,不过用的是它的子类 ValueKey,一般很少直接用LocalKey,而是使用它的三个直接子。依次介绍下~

2.3.1. ValueKey

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

内部维护一个 泛型value属性,重写了==和hashCode(),如果两个ValueKey的 value属性相等,则认为两个Key相等。

2.3.2. ObjectKey

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

内部维护一个 Object?类型的value属性,同样重写了==和hashCode(),如果两个ObjectKey的 value属性指向同一对象,则认为两个Key相等。

😃 总结下就是:ValueKey 判断 对象值 是否相等,ObjectKey 判断 对象引用 是否相等。

2.3.3. UniqueKey

跟🤡杰哥一起学Flutter (十、进阶-玩转各种Key🔑)

独一无二的Key,没有属性也没重写==和hashCode(),那就是比较 UniqueKey 对象本身是否 指向同一个内存地址咯 来判断Key是否相等。改下问题引入处的代码:

List<Widget> items = [
  TestKeyWidget(key: UniqueKey(), color: Colors.green),
  TestKeyWidget(key: UniqueKey(), color: Colors.blue),
  TestKeyWidget(key: UniqueKey(), color: Colors.red)
];

UniqueKey() 创建的Key唯一,所以组件的状态也得以保存。另外,它还有一个使用场景:

强制Flutter框架 不复用旧的Widget而是重新创建,每次都会走initState()初始化状态;

参考文献

转载自:https://juejin.cn/post/7324525547802574884
评论
请登录