【Flutter核心类分析】深入理解Key.

背景

我们在进行Flutter开发过程中,几乎每一个Widget都会有一个可选参数——Key。但是我们却很少去传这个值,既然我们可以不用传,那么这个Key作用到底是什么呢?

问题

下面我们先来看看这样一个场景:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: BodyWidget(),
    );
  }
}

class BodyWidget extends StatefulWidget {
  const BodyWidget({Key key}) : super(key: key);

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

class _BodyWidgetState extends State<BodyWidget> {
  List<Widget> list = [
    //上面两个显示StateLessColorBoxContainer
    StateLessColorBoxContainer(),
    StateLessColorBoxContainer(),
    //分割线
    Divider(
      height: 60.0,
      color: Colors.black,

    ),
		//下面两个显示StatefulColorBoxContainer
    StatefulColorBoxContainer(),
    StatefulColorBoxContainer(),
  ];

  void switchWidget() {
    setState(() {
      list.insert(0, list.removeAt(1));
      list.insert(3, list.removeAt(4));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: new Text("KeyDemo"),
      ),
      body: Container(
        child: Center(
          child: Column(
            children: list,
          ),
        ),
      ),
      floatingActionButton: ElevatedButton(
          onPressed: () {
            switchWidget();
          },
          child: Text("点击交换")),
    );
  }
}

class StateLessColorBoxContainer extends StatelessWidget {
  StateLessColorBoxContainer({Key key}) : super(key: key);

  final int random = Random().nextInt(4);
  final List<Color> colors = [
    Colors.red,
    Colors.yellow,
    Colors.green,
    Colors.grey
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: colors[random],
    );
  }
}

//定义StatefulColorBoxContainerContainer
class StatefulColorBoxContainer extends StatefulWidget {
  StatefulColorBoxContainer({Key key}) : super(key: key);

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

class _StatefulColorBoxContainerState extends State<StatefulColorBoxContainer> {
  int random = Random().nextInt(3);
  var colors = [Colors.red, Colors.yellow, Colors.green, Colors.grey];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: colors[random],
    );
  }
}

image-20211109222357816

源码比较好理解,上面两个显示两个StateLessColorBoxContainer,下面两个显示StatefulColorBoxContainer,两ColorBuildContainer类的build(context)方法完全一样,但是我们却发现点击下面的交换按钮,只有上面的每次进行了交换,而下面的不论点击多少次,都没有进行交换。

这是为什么呢,为什么使用StatefulWidget就不能成功交换更新呢?这就需要从Widget的更新机制说起来了。

Widget更新机制

在 Flutter 框架中,视图维持在树的结构中,我们编写的 Widget 一个嵌套一个,最终组合为一个 Tree。

StatelessWidget更新机制

在第一种使用 StatelessWidget 的实现中,当 Flutter 渲染这些 Widgets 时,Row Widget 为它的子 Widget 提供了一组有序的插槽。对于每一个 Widget,Flutter 都会构建一个对应的 Element。构建的这个 Element Tree 相当简单,仅保存有关每个 Widget 类型的信息以及对子Widget 的引用。你可以将这个 Element Tree 当做就像你的 Flutter App 的骨架。它展示了 App 的结构,但其他信息需要通过引用原始Widget来查找。

当我们交换行中的两个色块时,Flutter 遍历 Widget 树,看看骨架结构是否相同。它从 Row Widget 开始,然后移动到它的子 Widget,Element 树检查 Widget 是否与旧 Widget 是相同类型和 Key。 如果都相同的话,它会更新对新 widget 的引用。在我们这里,Widget 没有设置 Key,所以Flutter只是检查类型。它对第二个孩子做同样的事情。所以 Element 树将根据 Widget 树进行对应的更新。

当 Element Tree 更新完成后,Flutter 将根据 Element Tree 构建一个 Render Object Tree,最终开始渲染流程。

StatefulWidget更新机制

当使用 StatefulWidget 实现时,控件树的结构也是类似的,只是现在 color 信息没有存储控件自身了,而是在外部的 State 对象中。

现在,我们点击按钮,交换控件的次序,Flutter 将遍历 Element 树,检查 Widget 树中 Row 控件并且更新 Element 树中的引用,然后第一个 StatefulColorBoxContainer 控件检查它对应的控件是否是相同类型,它发现对方是相同的类型; 然后第二个 StatefulColorBoxContainer 控件做相同的事情,最终就导致 Flutter 认为这两个控件都没有发生改变。Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变,显示的内容也就不会改变。

那么如何解决这个StatefulWidget不更新的问题呢?这就需要用到Key了

StatefullWidget 结合 Key

StatefullWidget中有个方法canUpdate,我们先看下源码:

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
  ···
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

通过源码不难判断出上面的例子中,所有的canUpdate返回的都是true,也就是意味着他不回去重新创建Element,而是通过Widget配置信息去更新Element,但是呢上文的StatefulColorBoxContainer却没有保存color信息,所以导致Element也不会去更新。如果我们想要在runtimeType相同的情况下去正确的执行更新操作就只能使用到key了。每个widget如果key不同,也就是canUpdate返回为false,那么它对于的Element也就会重建,也就能够正确的执行交换了。

上面的例子中我们通过在StatefulColorBoxContainer加入key参数,就能正确的交换了。

  List<Widget> list = [
    StateLessColorBoxContainer(),
    StateLessColorBoxContainer(),
    //分割线
    Divider(
      height: 60.0,
      color: Colors.black,
    ),
		// 加入不同的key
    StatefulColorBoxContainer(key: UniqueKey()),
    StatefulColorBoxContainer(key: UniqueKey()),
  ];

Key的种类

Key 的目的在于为每个 Widget 指明一个唯一的身份,使用何种 Key 就要依具体的使用场景决定。

ValueKey

例如在一个 ToDo 列表应用中,每个 Todo Item 的文本是恒定且唯一的。这种情况,适合使用 ValueKey,value 是文本。

ObjectKey

假设,每个子 Widget 都存储了一个更复杂的数据组合,比如一个用户信息的地址簿应用。任何单个字段(如名字或生日)可能与另一个条目相同,但每个数据组合是唯一的。在这种情况下, ObjectKey 最合适。

UniqueKey

如果集合中有多个具有相同值的 Widget,或者如果您想确保每个 Widget 与其他 Widget 不同,则可以使用 UniqueKey。 在我们的例子中就使用了 UniqueKey,因为我们没有将任何其他常量数据存储在我们的色块上,并且在构建 Widget 之前我们不知道颜色是什么。

不要在 Key 中使用随机数,如果你那样设置,那么当每次构建 Widget 时,都会生成一个新的随机数,Element 树将不会和 Widget 树做一致的更新。

GlobalKeys

Global Keys有两种用途。

它们允许 Widget 在应用中的任何位置更改父级而不会丢失 State ,或者可以使用它们在 Widget 树 的完全不同的部分中访问有关另一个 Widget 的信息。

比如: 要在两个不同的屏幕上显示相同的 Widget,同时保持相同的 State,则需要使用 GlobalKeys。

在第二种情况下,您可能希望验证密码,但不希望与树中的其他 Widget 共享该状态信息,可以使用 GlobalKey 持有一个表单 Form 的 State。

总结

如何合理适当的使用 Key:

  1. When: 当您想要保留 Widget 树的状态时,请使用 Key。例如: 当修改相同类型的 Widget 集合(如列表中)时
  2. Where: 将 Key 设置在要指明唯一身份的 Widget 树的顶部
  3. Which: 根据在该 Widget 中存储的数据类型选择使用的不同类型的Key
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>