本文内容主要翻译自Keys in Flutter, 最初翻译动机是原作者写的比较通俗,其次 key 常识点在 Flutter 中比较重要,但在翻译进程中发现不合作相关源码很难了解作者意思并且看完简略忘,所以加了些注释和了解(详见引述),有什么不对的地方欢迎各位大佬沟通指正,多谢!


在运用 Flutter 时,咱们经常会遇到一个叫做 Key 的东西。Key 是 Flutter 中几乎一切 widget 都具有的特点。但它并不常用而简略被忽视。为什么 widget 具有 Key 呢?它对咱们有什么意义呢?让咱们找出答案。

什么是 Key

Flutter 将 Key 描绘为 Widget、Element 和 SemanticNodes 的标识符。这是什么意思呢?这意味着 Key 是分配给 Widget 的仅有标识,经过 key 能够与其他 Widget 区别开来。关于 Widget 在 Widget 树中改动方位的状况,Key 帮助保留它们的状况。阐明 Key 大多数状况下关于有状况的 Widget 而言更有用,而关于无状况的 Widget 则不太需求。

何时运用 Key

Key 能够放在代码的几乎任何地方而不会形成什么问题。但在不需求的状况下放 Key 只会糟蹋内存空间。因而,需求了解它的应用场景。

大部分状况下不需求运用 Key。在添加、删除或重排同一类型的 widget 调集时,Key 非常有用。这些 widget 保持某些状况,并且在 widget 树中处于相同的等级。假如没有 Key,更新这样的 widget 调集可能不会产生预期的成果。咱们倾向于在像 ListView 或 Stateful widget 的子级上运用 Key,由于其数据会不断变化。

为了进一步阐明修改 widget 调集时为什么需求 key,这儿用一个简略的示例阐明。示例显示了两个色彩块单击按钮时它们能够交流方位。

Flutter中的Key

该示例有两种完成方法

第一种完成:色块 widget 是无状况的,色值保存在 widget 本身中。当点击 FloatingActionButton,色块会像预期正确地交流方位。

import 'package:flutter/material.dart';
import 'package:keys_example/value_key_example.dart';
import 'package:random_color/random_color.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:const PositionTiles(),
    );
  }
}
class PositionTiles extends StatefulWidget {
  const PositionTiles({Key? key}) : super(key: key);
  @override
  State<PositionTiles> createState() => _PositionTilesState();
}
class _PositionTilesState extends State<PositionTiles> {
  List<Widget> tiles = [];
  @override
  void initState() {
    super.initState();
    tiles = [
       StatelessColorTiles(),
        StatelessColorTiles(),
    ];
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Row(mainAxisAlignment:MainAxisAlignment.center,children: tiles,),),
      floatingActionButton: FloatingActionButton(child: Icon(Icons.sentiment_very_satisfied, ),onPressed: swapTiles,),
    );
  }
  void swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}
///色块widget是无状况的
class StatelessColorTiles extends StatelessWidget {
//色值保存在本身控件中
 Color myColor = RandomColor().randomColor();
  @override
  Widget build(BuildContext context) {
    return  Container(
      height: 100,
      width: 100,
      color: myColor,
    );
  }
}

第二种完成:色块 widget 是有状况的,并将色值保存在状况中。这一次,当点击 FloatingActionButton 时似乎什么都没有产生。为了正确交流平铺方位,咱们需求向有状况的 widget 添加 key 参数。

import 'package:flutter/material.dart';
import 'package:keys_example/value_key_example.dart';
import 'package:random_color/random_color.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:const PositionTiles(),
    );
  }
}
class PositionTiles extends StatefulWidget {
  const PositionTiles({Key? key}) : super(key: key);
  @override
  State<PositionTiles> createState() => _PositionTilesState();
}
class _PositionTilesState extends State<PositionTiles> {
  List<Widget> tiles = [];
  @override
  void initState() {
    super.initState();
    tiles = [
        //添加了key参数,若不添加则点击按钮色块不会交互
        StatefulColorTiles(key: UniqueKey(),),
        StatefulColorTiles(key: UniqueKey(),),
    ];
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Row(mainAxisAlignment:MainAxisAlignment.center,children: tiles,),),
      floatingActionButton: FloatingActionButton(child: Icon(Icons.sentiment_very_satisfied, ),onPressed: swapTiles,),
    );
  }
  void swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}
class StatefulColorTiles extends StatefulWidget {
  const StatefulColorTiles({Key? key}) : super(key: key);
  @override
  State<StatefulColorTiles> createState() => _StatefulColorTilesState();
}
class _StatefulColorTilesState extends State<StatefulColorTiles> {
  ///色值保存在state中
  late Color myColor;
  @override
  void initState() {
    super.initState();
    RandomColor _randomColor = RandomColor();
    myColor = _randomColor.randomColor();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
        height: 100,
        width: 100,
        color: myColor,
     );
  }
}

总结:该示例表明,仅当 widget 有状况时,才需求设置 key。假如是无状况的 widget 则不需求设置 key。

背后原理

刚刚第二种完成中,运用 key 的代码中完成预期的行为。为什么 key 能够做到这一点呢?让咱们来找出答案。

当烘托 widget 时,Flutter 不只会构建 widget 树,同时也会构建其对应的元素树。元素树持有 widget 树中 widget 的信息及其子 widget 的引证。在修改和从头烘托的进程中,Flutter 查找元素树以查看其是否已改动,以便在元素未改动时能够复用旧元素。

阐明

① widget 树相当于装备,元素树相当于实例目标。widget 相当于 json,元素树相当于 json 解析后的 bean。

② 关于改动的判断条件 : widget 类型 和 key 值 ,若在没用 key 的状况下,若类型相同则表明新旧 widget 可复用

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

在本例中,原色块是 A 和 B, 交互后 oldWidget = A newWidget = B 由于 A 和 B 是同类型 StatelessColorTiles ,则表明 A 在原来在元素树中的 E(A)元素在交流后是能够继续供 B 复用。

前置常识

W(A)和 W(B)交流后调用 setState 结构产生了啥?

将本身元素目标标记为脏元素并放到脏元素数组中,期间会触发 Vsync 信号,等候体系更新脏元素数组中的> 元素。

整个进程会递归履行,由于 build 方法中是嵌套关系,会一层层遍历来履行如下进程,如下:

@protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  	//省掉过程....
    if (child != null) {
      // 两个widget相同,方位不同更新方位,回来child。这儿比较的是hashCode
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      // 咱们的交流例子处理在这儿
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    // 假如无法更新复用,那么创立一个新的Element并回来。
    return inflateWidget(newWidget, newSlot);
  }

无状况的示例中,每个色块 widget 都有其对应的色块元素。由于色值特点保存在 widget 本身中,当交流色块 widget 时,元素树上的引证没变依然是原来色块元素。因而,正确交互完成预期行为。

很显然,假如 W(A) 与 W(B)在交互后,canUpdate 回来 true。此刻履行 child.update(newWidget),其间 child = Element(A)
child.widget = W(A)
newWidget = W(B) 即履行:Elelement(A).update(W(B))

由于 W(B) 中保存了 B 的色值,色值随身携带原因,一切到达预期行为,W(A)与 W(B)在交互后 setState 交互了色彩。

有状况的示例中,每个色块 widget 都有其对应的色块元素,且该元素都包含了 State 特点。当交流色块 widget 时,它们持有 State 特点原因相应的元素匹配不上,而期望的行为没有完成。

仍是上面的过程: 很显然,假如 W(A) 与 W(B)在交互后,canUpdate 回来 true。此刻履行 child.update(newWidget),其间 child = Element(A)
child.widget = W(A)
newWidget = W(B)

不同点:由于 W(B) 中保存的不是色值而是 state 特点(人【W(B)】尽管嫁给你了,可是心【color】不属于你),所以没法更 新成功了。

在将 key 添加到色块 widget 中后,元素树和 widget 树会运用键值进行更新。当咱们交流色块时,色块元素能够凭借它们的 key 在 widget 树中找到它们相应的 widget,并正确地更新它们的引证,从而使 widget 正确地交流方位当按下按钮时更新其色彩。

当设置了 key 后 canUpdate = false,原因:尽管类型相同了,可是 oldWidget.key != newWidget.key

@protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  	//省掉过程....
    //省掉过程...
      // 咱们的交流例子处理在这儿
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    // 假如无法更新复用,那么创立一个新的Element并回来。
    return inflateWidget(newWidget, newSlot);
  }

这样 updateChild 就直接履行 return inflateWidget(newWidget, newSlot); 逻辑。

已然你心不在我身上,就再找一个吧,迁就在一起也没意思。

加了 key 的 W(A)和 W(B) 交流后体系更新时,不会复用原来元素树中的 Element(A) ,而是经过 W(B)从头创立一个新的 Element(B)。从头构建连带 state 中色值变量也会同步更新,达预期行为。

至此,这就是 key 怎么在内部工作以及其在修改调集中有状况 widget 方面的用处。

键类型

Key 一般分两种类型:

  1. 本地类型
  2. 全局类型

本地键

在拥有相同父元素的元素中有必要是共同的。本地键能够进一步分类如下:

比如同一个父节点下的孩子节点之间是共同存在的。

值键

值 Key 承受字母数字值。它们通常用于子列表中,其间每个子项的值是仅有且恒定的。

目标键

与值键相同,仅有的区别是它承受一个包含数据的类目标。

仅有键

在子 widget 没仅有值或根本没值的状况下,运用仅有键来标识子部件。

上面三个类型中提到的值说的是控件上承载的一些数据值。经过这些值类型来结构相关于的 Key。

页面存储

该键用来保留用户在滚动视图中的滚动方位,以便以后能够保存。

参考链接

说说 Flutter 中最了解的陌生人 —— Key