likes
comments
collection
share

Flutter之Key的原理解析

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

前言

在Flutter开发中,会发现在很多的组件的构造函数中都会有一个可选的参数Key,你可能会疑惑这个Key的作用是什么?这篇文章就来揭开Key的面纱。

1、案例解析

先不着急去看Key是什么?我们先看一下下面这段代码的运行。

class _KeyDemoState extends State<KeyDemo> {
  List<Widget> itemList = [
    StatelessItem("A"),
    StatelessItem("B"),
    StatelessItem("C"),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("KeyDemo"),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: itemList,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            itemList.removeAt(0);
          });
        },
      ),
    );
  }
}

class StatelessItem extends StatelessWidget {
  final String _title;
  StatelessItem(this._title);
  final _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: _color,
      child: Text(_title),
      alignment: Alignment.center,
    );
  }
}

其实这段代码很简单,就是在界面上显示三个不同颜色的Container,点击按钮依次删除第一个色块。

Flutter之Key的原理解析

如上所示结果正如我们所诉求的一样,并无不妥,那么如果我们将StatelessItem替换成StatefulWidget的widget又当如何呢?

class StatefulItem extends StatefulWidget {
  final String _title;
  StatefulItem(this._title);
  
  @override
  _StatefulItemState createState() => _StatefulItemState();
}

class _StatefulItemState extends State<StatefulItem> {
  final _color = Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: _color,
      child: Text(widget._title),
      alignment: Alignment.center,
    );
  }
}

Flutter之Key的原理解析

正如上所示的效果,在操作的过程中出现了错乱,本应该已删除的第一个色块的颜色显示在了第二个色块的部分。那么为什么会出现这样的现象?这和Flutter的Widgetdiff更新机制有莫大的关系。

2、Widget更新

如果你对于Flutter有一定了解,那么一定知道Flutter中的三棵树Widget Tree,Element Tree,Render Tree共同铸就Flutter的渲染流程,关于Flutter的渲染流程会在后续的文章中详细介绍。

在Flutter中使用Widget来构建你的UI,Widget描述了他们的视图在给定其当前配置和状态时的样式,当widget的状态发生变化时,widget会重新构建UI。看起来重新构建UI的过程是重新渲染Widget树的过程,实际上Widget只是想当于一个配置清单,Element才是被使用的对象,重新渲染视图是对Element树的渲染过程。为了性能上的考虑,重新渲染的先决条件是判断两个新老widget的runtimeTypekey是否一致。如果一致则说明不需要替换Element,直接更新widget就可以了。

  /// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

2.1、StatelessItem的比较过程

在第一个代码里面的Container是一个StatelessWidget的widget,同时也没有传入key到构造函数中,所以对于canUpdate方法而言,只需要比较新老widget的runtimeType即可,很明显这里的runtimeType是一致的,所以会返回true。当点击按钮删除第一个Container时,StatelessElement便会调用新持有Widget的build方法重新构建UI,所以便可以看到色块是按顺序删除的。

2.2、StatefulItem的比较过程

同理在StatefulWidget的Container中,也没有传入key,所以对于canUpdate方法而言,只需要比较新老widget的runtimeType即可,很明显这里的runtimeType是一致的,所以会返回true。而不同的是颜色是在State中的,在点击按钮删除一个Container时,会重新build构建UI,但是不会删除Element,只是从原持有的State实例中build新的widget。因为Element没有变,所以State不会变化,那么颜色也不会变化。

而如果给Widget一个key之后,canUpdate方法将会返回false,即表示需要重新构建Element。此时RenderObjectElement会用新Widget的key在老Element列表里面查找,找到匹配的则会更新Element的位置并更新对应renderObject的位置,对于这里的代码而言就是删除对应的Element。

Flutter之Key的原理解析

整个过程如上图所示,原本的Wiget和Element之间的关系如黑色连线所示,待删除Widget A后,它们之间的关系便会如红色连线所示。

传入key后的效果如下:

List<Widget> itemList = [
    StatefulItem("A", key: ValueKey(1)),
    StatefulItem("B", key: ValueKey(2)),
    StatefulItem("C", key: ValueKey(3)),
  ];

Flutter之Key的原理解析

3、Key的分类

abstract class Key {
  const factory Key(String value) = ValueKey<String>;
  @protected
  const Key.empty();
}

Key本身是一个抽象类,由此派生出两种不同用途的Key:LocalKeyGlobalKey

3.1、LocalKey

LocalKey直接继承自Key,是用作Widget刷新的diff算法的核心所在,用作Element和Widget进行比较。 LocalKey又派生出很多的子类:

  • ValueKey:以一个数据作为Key。如:数字、字符等;
  • ObjectKey:以Object对象作为Key;
  • UniqueKey:可以保证Key的唯一性;(一旦使用Uniquekey那么就不存在Element复用 了!)

3.1、GlobalKey

GlobalKey可以用来标识唯一子widget。GlobalKey在整个widget结构中必须是全局唯一的,而不像LocalKey只需要在兄弟widget中唯一。由于它们是全局唯一的,因此可以使用GlobalKey来获取到对应的Widget的State对象!