Flutter的Key是什么?有什么作用

806 阅读7分钟

大多数情况下,我们不会给Widget设置key,但涉及到列表删除和插入时,通常需要使用key,如果不使用可能会导致列表错乱

1. Key的使用示例

我们先通过以下简单的示例,我们思考一下key的作用是什么,以及出现这些现象的原因

1.1. StatelessWidget

  • BoxLessWiget 背景颜色是随机生成的
  • StatelessWidget是无状态的,且Widget都是不可变的,每次都会重新构建,因此颜色会随机变化
Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      BoxLessWiget(title: 'box1'),
      BoxLessWiget(title: 'box2'),
    ],
  ),
),
void swapBoxs() { // 点击按钮执行这个方法
  setState(() {});
}
import 'package:flutter/material.dart';
import 'package:key_study/tools.dart';

class BoxLessWiget extends StatelessWidget {
  const BoxLessWiget({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    print("BoxLessWiget build");
    return Container(
      width: 100,
      height: 100,
      alignment: Alignment.center,
      color: getRandomColor(), // 随机生成颜色
      child: Text(
        title,
        style: const TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}
  • StatelessWidget加上const,表明Widget 是常量,因此flutter在构建时会直接复用,因此颜色不会在改变
children: [
  const BoxLessWiget(title: 'box1'),
  const BoxLessWiget(title: 'box2'),
],
  • 我们把children存储到状态中,此时我们点击按钮,因为boxs没有变化,所以色块不会变
  List<Widget> boxes = [
    const BoxLessWiget(title: 'box1'),
    const BoxLessWiget(title: 'box2'),
  ];
  void swapBoxs() {
    setState(() {});
  }
  • 如果交换两个色块,颜色会随机变化
  • 虽然BoxLessWigetconstant,但boxes不是constant,所以boxs状态变化了,重新执行buildbox颜色就会随机刷新
  void swapBoxs() {
    boxes.insert(1, boxes.removeAt(0)); 
    setState(() {});
  }
  • 思考:给box加上key,颜色不会随机变化了,并且正常交换色块了
 List<Widget> boxes = [
    BoxLessWiget(
      title: 'box1',
      key: ValueKey(1),
    ),
    BoxLessWiget(
      title: 'box2',
      key: ValueKey(2),
    ),
  ];

1.2. StatefulWidget

上面例子中,我们还可以通过state来管理颜色状态,这样每次biuld就不会重新生成颜色了

  • 我们将颜色保存到了state中了,这样每次刷新,虽然会重新执行build,但state中的数值没有改变,所以色块颜色不会变
  • 当然如果我们加上constbuild都不会执行了,颜色更不可能变化了
children: [
  BoxFulWiget(title: 'box1'),
  BoxFulWiget(title: 'box2'),
],
  void swapBoxs() {
    setState(() {});
  }
import 'package:flutter/material.dart';
import 'package:key_study/tools.dart';

class BoxFulWiget extends StatefulWidget {
  const BoxFulWiget({
    super.key,
    required this.title,
  });
  final String title;

  @override
  State<BoxFulWiget> createState() => _BoxFulWigetState();
}

class _BoxFulWigetState extends State<BoxFulWiget> {
  Color backgroundColor = getRandomColor(); // 先将颜色存储起来

  @override
  Widget build(BuildContext context) {
    print("BoxFulWiget build");

    return Container(
      width: 100,
      height: 100,
      alignment: Alignment.center,
      color: backgroundColor,
      child: Text(
        widget.title,
        style: const TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}
  • 接下来观察另外一个现象:我们交换两个box,色块没有交换
  List<Widget> boxes = [
    const BoxFulWiget(title: 'box1'),
    const BoxFulWiget(title: 'box2'),
  ];
void swapBoxs() {
  boxes.insert(1, boxes.removeAt(0));
  setState(() {});
}
  • 我们多加一个box,移除第一个box,但是移除的却是最后一个
  List<Widget> boxes = [
    const BoxFulWiget(title: 'box1'),
    const BoxFulWiget(title: 'box2'),
    const BoxFulWiget(title: 'box3'),
  ];
  void swapBoxs() {
    boxes.removeAt(1);
    setState(() {});
  }
  • 思考:给box加上key后,就能正常交换了,移除也能正常移除了
  List<Widget> boxes = [
    BoxFulWiget(
      title: 'box1',
      key: ValueKey(1),
    ),
    BoxFulWiget(
      title: 'box2',
      key: ValueKey(2),
    ),
  ];
  void swapBoxs() {
    boxes.insert(1, boxes.removeAt(0));
    // boxes.removeAt(1);
    setState(() {});
  }
  • 思考另一个现象:我们再给BoxFulWiget加上一个Padding包裹,此时交换boxes的位置,点击按钮会发现box1的颜色随机变化了。
  List<Widget> boxes = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: BoxFulWiget(
        title: 'box1',
        key: ValueKey(1),
      ),
    ),
    BoxFulWiget(
      title: 'box2',
      key: ValueKey(2),
    ),
  ];
boxes.insert(1, boxes.removeAt(0));

2. Flutter 渲染流程

想要理解以上的现象,我们先了解一下flutter的渲染流程,key主要涉及Widget 树和Element 树。

流程:Widget 树 => Element 树 => RenderObject 树 => paint() 绘制

2.1. Widget 树

  • 不可变Widget 是不可变的,一旦创建无法改变。每次改变时,都会创建一个新的 Widget 实例。
  • 轻量级Widget 只是描述信息,不存储实际的布局和绘制数据,因此它非常轻量。
  • 如下: ColumnTextRaisedButton 都是 Widget
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('Hello'),
      RaisedButton(onPressed: () {}),
    ],
  );
}

2.2. Element 树

  • 负责管理生命周期ElementWidget 的具体实现,保持 WidgetRenderObject 的生命周期。每次调用 setStatebuild 时,都会在 Element 中进行更新。
  • 如果一个 Widget 在父树中发生变化,Element 会检测到这些变化,并调用相应的布局、绘制逻辑。
  • 如下:MyWidget 被创建时,会产生一个 Element 实例,这个 Element 用来管理 ColumnText 这些 Widget
class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [Text('Hello')],
    );
  }
}

2.3. RenderObject 树

  • RenderObject 树是 Element 树的一个子集。当 Element 被创建后,Element 会创建一个对应的 RenderObject,并将其与父 RenderObject 进行连接。
  • RenderObject 会根据父节点传递的约束来计算布局,并进行绘制。

3. Element何时会重建

Element如果销毁重新创建,会重新执行widget的生命周期,初始化新的 State

3.1. keyruntimeType 不同时

  • 以下是flutter源码片段,可以看到主要是对比类型runtimeTypekey是否相同
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}
  • 我们再看看,BoxFulWiget不加key,删除第一个widget时,发生了什么
    • box1删除后,box2box1element类型相同,因此box1element不会被删除,并变成了box2element
    • 依次对比,box2element变成了box3element
    • 最终box3element没有对应的widget,所以box3element就被销毁了
    • 所以显示的是box2-color1box3-color2

  • 看看上面给BoxFulWiget加上key后,为什么能正常交换了
    • 交换位置后box2box1elementkey不相同,再往下对比
    • 找到box2对应的element,因此box2element往上插入
    • box1找到box1,最终实现位置交换

3.2. 同层级对比

  • 再思考:为什么加上padding后,box1的颜色随机变化了
    • 交换位置后box2Padding的类型不一致,且Padding没有key,因此Padding Element就会被销毁掉
    • box2在往下找到了box2element,并且key一致,因此box2element往上移
    • Padding Widget再对比时,没有找到对应的element,因此会重新创建一个,那box1的颜色就会重新生成

4. Key的类型

4.1. Globalkey 全局key

  • 作用GlobalKey 可以跨 Widget 树访问 State
  • 特点: 唯一,性能开销较大
  • 常见用途
    • 访问 Scaffold:比如控制 ScaffoldshowSnackBar()openDrawer() 等方法。
    • 表单验证:你可以使用 GlobalKey 来获取 FormState,例如在用户提交表单时进行验证。
  • 示例:上面示例中,给BoxFulWiget,加上paddind后颜色随机变了,但此时我们把ValueKey换成GlobalKey(),我们发现切换位置,box1的颜色就不会随机变化了
 List<Widget> boxes = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: BoxFulWiget(
        title: 'box1',
        key: GlobalKey(), // GlobalKey可以跨Widget树进行对比
      ),
    ),
    BoxFulWiget(
      title: 'box2',
      key: ValueKey(2),
    ),
  ];

4.2. LocalKey 局部key

Localkey是一个抽象类,不能直接实例化,他主要有以下三种类型

4.2.1. ValueKey

  • 根据某个固定值区分 Widget,通常适用在列表中,使用idname等字段来确定key
  • 示例:
  List<Widget> boxes = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: BoxFulWiget(
        title: 'box1',
        key: ValueKey(1), 
      ),
    ),
    BoxFulWiget(
      title: 'box2',
      key: ValueKey(2),
    ),
  ];

4.2.2. UniqueKey

  • 每次生成不同的唯一 Key,每次更新都会生成不同的key
  • 以下情况,key在对比时,是不相同的
var key1 =  UniqueKey()
var key2 =  UniqueKey()  // key1和key2是两个key
  • 比如在以下情况下,每次点击按钮时,颜色都会随机生成
void swapBoxs() {
  boxes = [
    BoxFulWiget(
      title: 'box2',
      key: UniqueKey(),
    ),
    BoxFulWiget(
      title: 'box1',
      key: UniqueKey(),
    ),
  ];
  setState(() {});
}

4.2.3. ObjectKey

  • 基于对象引用区分 Widget
  • ObjectKey 是用对象的引用来创建唯一Key。它的比较的是对象的引用地址,而不是对象的内容。
  • ValueKey如何选择:
    • 当你需要使用某个值的内容(比如 idname等)来唯一标识 Widget 时,ValueKey 是最合适的选择。
    • 当你需要根据对象的引用来唯一标识 Widget,或者你正在操作的对象的值会发生变化,但对象引用本身不变时,ObjectKey 更适合。

5. 参考

www.youtube.com/watch?v=kn0…