23.Key

Key 的作用

我们创建这样一个示例:

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: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  //key的作用就非常大了!!
  List<Widget> items = [
    ColorItem('第1个'),
    ColorItem('第2个'),
    ColorItem('第3个'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Key的作用'),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: items,
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          setState(() {
            items.removeAt(0);
          });
        },
      ),
    );
  }
}

class ColorItem extends StatefulWidget {
  final String title;

  ColorItem(this.title, {Key? key}) : super(key: key);

  @override
  _ColorItemState createState() => _ColorItemState();
}

class _ColorItemState extends State<ColorItem> {
  final color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Text(widget.title),
      color: color,
    );
  }
}
1

运行发现了上面的现象,作为有 iOS 开发经验的我们,很容易联想到这应该是和复用有关的现象。

复用问题

这里我们需要理解, ColorItem 作为一个 StatefulWidget 它是由两部分组成的: WidgetState 。这里就是 Widget 被移除后, State 还在内存中。所以出现了复用异常的问题。

那么将数据放在 Widget 中按照这里分析的理论来说应该就没有这个现象了吧?我们试试:

2

验证成功!那么具体原因是什么呢?

增量渲染与 canUpdate

我们来到 Widget 的实现中, canUpdate 方法决定了一个 WidgetElement 是否会被更新,而 Element 的更新,又直接关系到了增量渲染

同时满足两个条件:

  • 新旧 Widget 的类型相同

  • 新旧 WidgetKey 相同

    • 如果 Key 是空的,只通过类型判断,就算他们的子 Widget 完全不同

这里就很好的解释了一开始发生的异常现象。

图解

WidgetElement 是一一对应的,而 State 是在 Element 中的。

4
  • 移除一个 Widget 开始检查

  • 第一个 Element 检查到第二个 Widget

    • 调用 canUpdate

      • 类型相同

      • Key 为空

      • return ture

    • 于是就用 第一个Element 更新 第二个Widget

  • 依次类推,就发生了前面的异常现象

5

既然这里提到了 Key ,那么加上 Key 是否就可以上面这个问题呢?

Key 的使用

这里为每个 ColorItem 加上 Key , 并将 Color 属性放回 State 中:

3

有效!

Key

Key 本身是一个抽象类,有一个工厂方法:

  • 它有两个子类

    • LocalKey

      • 同一个 父Element 内唯一的

    • GlobalKey

      • 整个 App 内唯一

刚才用的 ValueKeyLocalKey 的子类。

LocalKey

区别哪个 Element 要保留,哪个 Element 要删除。

  • ValueKey

    • 以值作为参数(数字、字符串等)

  • ObjectKey

    • 以对象作为参数

  • UniqueKey

    • 创建唯一标识

GlobalKey

在整个 App唯一的KeyGlobalKey 可以唯一标识一个元素, 比如访问一个 BuildContext 或者一个 Widget。对于 StatefulWidget 而言, GlobalKey 也可以访问 State

当有 GlobalKeyWidget 被移动到 Widget树 中新的位置的话,会重新渲染他们的子树。

为了渲染子树,一个 Widget 必须在同一个动画帧内完成在树中,从旧位置移动到新位置。

重新渲染一个使用 GlobalKeyElement 是很消耗性能的,因为会触发调用所有相关的 Statedeactivate 方法,然后让所有依赖 InheritedWidgetWidget 重建。

所以如果你不需要达到上面的效果,那么建议使用其他的 Key

注意点

  • 两个在同一个树中的 Widget 不能同时有相同的 GlobalKey 。尝试这样做的话在运行时会触发异常。

  • GlobalKeys 不应该再每次 build 的时候被重新创建。他们应该是长期被一个 State 所持有的。

    • 例如:

      • 每次 build 的时候都创建一个新的 GlobalKey 会丢弃和 旧Key 相关的子树,并为新Key创建一个新树。除了会损害性能,这种操作还有可能会对子树造成未知的影响。

      • 例如子树中的 GestureDetector 将无法继续追踪正在进行中的手势,因为它会在每次 build 的时候被重新创建

  • 比较好的做法是:

    • 让一个 State 持有这个 GlobalKey ,并且在 Build 方法外初始化它

    • 比如在 State.initState

GlobalKey简单示例

Last updated

Was this helpful?