Flutter | 深刻浅出 Key

前言

在开发 Flutter 的过程当中你可能会发现,一些小部件的构造函数中都有一个可选的参数——Key。刚接触的同窗或许会对这个概念感到很迷茫,感到不知所措。算法

在这篇文章中咱们会深刻浅出的介绍什么是 Key,以及应该使用 key 的具体场景。less

什么是Key

在 Flutter 中咱们常常与状态打交道。咱们知道 Widget 能够有 Stateful 和 Stateless 两种。Key 可以帮助开发者在 Widget tree 中保存状态,在通常的状况下,咱们并不须要使用 Key。那么,究竟何时应该使用 Key呢。dom

咱们来看看下面这个例子。ide

class StatelessContainer extends StatelessWidget {
  final Color color = RandomColor().randomColor();
  
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: color,
    );
  }
}

这是一个很简单的 Stateless Widget,显示在界面上的就是一个 100 * 100 的有颜色的 Container。 RandomColor 可以为这个 Widget 初始化一个随机颜色。函数

咱们如今将这个Widget展现到界面上。布局

class Screen extends StatefulWidget {
  @override
  _ScreenState createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatelessContainer(),
    StatelessContainer(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: widgets,
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: switchWidget,
        child: Icon(Icons.undo),
      ),
    );
  }

  switchWidget(){
    widgets.insert(0, widgets.removeAt(1));
    setState(() {});
  }
}

这里在屏幕中心展现了两个 StatelessContainer 小部件,当咱们点击 floatingActionButton 时,将会执行 switchWidget 并交换它们的顺序。post

看上去并无什么问题,交换操做被正确执行了。如今咱们作一点小小的改动,将这个 StatelessContainer 升级为 StatefulContainer。性能

class StatefulContainer extends StatefulWidget {
  StatefulContainer({Key key}) : super(key: key);
  @override
  _StatefulContainerState createState() => _StatefulContainerState();
}

class _StatefulContainerState extends State<StatefulContainer> {
  final Color color = RandomColor().randomColor();

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

在 StatefulContainer 中,咱们将定义 Color 和 build 方法都放进了 State 中。ui

如今咱们仍是使用刚才同样的布局,只不过把 StatelessContainer 替换成 StatefulContainer,看看会发生什么。this

这时,不管咱们怎样点击,都再也没有办法交换这两个Container的顺序了,而 switchWidget 确实是被执行了的。

为了解决这个问题,咱们在两个 Widget 构造的时候给它传入一个 UniqueKey。

class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: UniqueKey(),),
    StatefulContainer(key: UniqueKey(),),
  ];
  ···

而后这两个 Widget 又能够正常被交换顺序了。

看到这里你们确定心中会有疑问,为何 Stateful Widget 没法正常交换顺序,加上了 Key 以后就能够了,在这之中到底发生了什么? 为了弄明白这个问题,咱们将涉及 Widget 的 diff 更新机制。

Widget 更新机制

在以前的文章中,咱们介绍了 WidgetElement 的关系。若你还对 Element 的概念感到很模糊的话,请先阅读 Flutter | 深刻理解BuildContext

下面来来看Widget的源码。

@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;
  }
}

咱们知道 Widget 只是一个配置且没法修改,而 Element 才是真正被使用的对象,并能够修改。

当新的 Widget 到来时将会调用 canUpdate 方法,来肯定这个 Element是否须要更新。

canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否须要更新。若 canUpdate 方法返回 true 说明不须要替换 Element,直接更新 Widget 就能够了。

StatelessContainer 比较过程

在 StatelessContainer 中,咱们并无传入 key ,因此只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,StatelessElement 调用新持有 Widget 的 build 方法从新构建,在屏幕上两个 Widget 便被正确的交换了顺序。

StatefulContainer 比较过程

而在 StatefulContainer 的例子中,咱们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。

当咱们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。因为两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 true,因而更新 StatefulWidget 的位置,这两个 Element 将不会交换位置。可是原有 Element 只会从它持有的 state 实例中 build 新的 widget。由于 element 没变,它持有的 state 也没变。因此颜色不会交换。这里变换 StatefulWidget 的位置是没有做用的。

而咱们给 Widget 一个 key 以后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 false。(这里 runtimeType 相同,key 不一样)

此时 RenderObjectElement 会用新 Widget 的 key 在老 Element 列表里面查找,找到匹配的则会更新 Element 的位置并更新对应 renderObject 的位置,对于这个例子来说就是交换了 Element 的位置并交换了对应 renderObject 的位置。都交换了,那么颜色天然也就交换了。

这里感谢ad6623对以前错误描述的指出。

比较范围

为了提高性能 Flutter 的比较算法(diff)是有范围的,它并非对第一个 StatefulWidget 进行比较,而是对某一个层级的 Widget 进行比较。

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(key: UniqueKey(),),
    ),
  ];
···

在这个例子中,咱们将两个带 key 的 StatefulContainer 包裹上 Padding 组件,而后点击交换按钮,会发生下面这件奇妙的事情。

两个 Widget 的 Element 并非交换顺序,而是被从新建立了。

在 Flutter 的比较过程当中它下到 Row 这个层级,发现它是一个 MultiChildRenderObjectWidget(多子部件的 Widget)。而后它会对全部 children 层逐个进行扫描。

在Column这一层级,padding 部分的 runtimeType 并无改变,且不存在 Key。而后再比较下一个层级。因为内部的 StatefulContainer 存在 key,且如今的层级在 padding 内部,该层级没有多子 Widget。runtimeType 返回 flase,Flutter 的将会认为这个 Element 须要被替换。而后从新生成一个新的 Element 对象装载到 Element 树上替换掉以前的 Element。第二个 Widget 同理。

因此为了解决这个问题,咱们须要将 key 放到 Row 的 children 这一层级。

···
class _ScreenState extends State<Screen> {
  List<Widget> widgets = [
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulContainer(),
    ),
  ];
···

如今咱们又能够愉快的玩耍了(交换 Widget 顺序)了。

扩展内容

slot 可以描述子级在其父级列表中的位置。多子部件 Widget 例如 Row,Column 都为它的子级提供了一系列 slot。

在调用 Element.updateChild 的时候有一个细节,若新老 Widget 的实例相同,注意这里是实例相同而不是类型相同, slot 不一样的时候,Flutter 所作的仅仅是更新 slot,也就给他换个位置。因 为 Widget 是不可变的,实例相赞成味着显示的配置相同,因此要作的仅仅是挪个地方而已。

abstract class Element extends DiagnosticableTree implements BuildContext {
···
  dynamic get slot => _slot;
  dynamic _slot;
···
 @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ···
    if (child != null) {
      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);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }
      deactivateChild(child);
      assert(child._parent == null);
    }
    return inflateWidget(newWidget, newSlot);
  }

更新机制表 | | 新WIDGET不为空 | 新 Widget不为空 | | :-------------: | :------------------------ | :----------------------------------------------------------- | | child为空 | 返回null。 | 返回新的 Element | | child不为空 | 移除旧的widget,返回null. | 若旧的child Element 能够更新(canUpdate)则更新并将其返回,不然返回一个新的 Element. |

Key 的种类

Key

@immutable
abstract class Key {
  const factory Key(String value) = ValueKey<String>;

  @protected
  const Key.empty();
}

默认建立 Key 将会经过工厂方法根据传入的 value 建立一个 ValueKey。

Key 派生出两种不一样用途的 Key:LocalKey 和 GlobalKey。

Localkey

LocalKey 直接继承至 Key,它应用于拥有相同父 Element 的小部件进行比较的状况,也就是上述例子中,有一个多子 Widget 中须要对它的子 widget 进行移动处理,这时候你应该使用Localkey。

Localkey 派生出了许多子类 key:

  • ValueKey : ValueKey('String')
  • ObjectKey : ObjectKey(Object)
  • UniqueKey : UniqueKey()

Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')

GlobalKey

@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
···
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
···
BuildContext get currentContext ···
Widget get currentWidget ···
T get currentState ···

GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。

你能够经过 GlobalKey 找到持有该GlobalKey的 WidgetStateElement

注意:GlobalKey 是很是昂贵的,须要谨慎使用。

何时须要使用 Key

ValueKey

若是您有一个 Todo List 应用程序,它将会记录你须要完成的事情。咱们假设每一个 Todo 事情都各不相同,而你想要对每一个 Todo 进行滑动删除操做。

这时候就须要使用 ValueKey!

return TodoItem(
    key: ValueKey(todo.task),
    todo: todo,
    onDismissed: (direction){
        _removeTodo(context, todo);
    },
);

ObjectKey

若是你有一个生日应用,它能够记录某我的的生日,并用列表显示出来,一样的仍是须要有一个滑动删除操做。

咱们知道人名可能会重复,这时候你没法保证给 Key 的值每次都会不一样。可是,当人名和生日组合起来的 Object 将具备惟一性。

这时候你须要使用 ObjectKey!

UniqueKey

若是组合的 Object 都没法知足惟一性的时候,你想要确保每个 Key 都具备惟一性。那么,你可使用 UniqueKey。它将会经过该对象生成一个具备惟一性的 hash 码。

不过这样作,每次 Widget 被构建时都会去从新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件仍是会改变。(还不如不用😂)

PageStorageKey

当你有一个滑动列表,你经过某一个 Item 跳转到了一个新的页面,当你返回以前的列表页面时,你发现滑动的距离回到了顶部。这时候,给 Sliver 一个 PageStorageKey!它将可以保持 Sliver 的滚动状态。

GlobalKey

GlobalKey 可以跨 Widget 访问状态。 在这里咱们有一个 Switcher 小部件,它能够经过 changeState 改变它的状态。

class SwitcherScreenState extends State<SwitcherScreen> {
  bool isActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Switch.adaptive(
            value: isActive,
            onChanged: (bool currentStatus) {
              isActive = currentStatus;
              setState(() {});
            }),
      ),
    );
  }

  changeState() {
    isActive = !isActive;
    setState(() {});
  }
}

可是咱们想要在外部改变该状态,这时候就须要使用 GlobalKey。

class _ScreenState extends State<Screen> {
  final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SwitcherScreen(
        key: key,
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        key.currentState.changeState();
      }),
    );
  }
}

这里咱们经过定义了一个 GlobalKey<SwitcherScreenState> 并传递给 SwitcherScreen。而后咱们即可以经过这个 key 拿到它所绑定的 SwitcherState 并在外部调用 changeState 改变状态了。

参考资料

写在最后

这篇文章的灵感来自于 什么时候使用密钥 - Flutter小部件 101 第四集, 强烈建议你们观看这个系列视频,你会对 Flutter 如何构建视图更加清晰。也但愿这篇文章对你有所帮助!

在这个视频最后介绍 GlobalKey 时,提到了 Globalkey 可以用于在不一样小部件之间同步状态,以及保存状态的功能,但我并无找到实现办法,若是有使用过这两个功能的小伙伴麻烦在这篇文章下面留言告诉我一下,谢谢!😙

文章如有不对之处还请各位高手指出,欢迎在下方评论区以及个人邮箱1652219550a@gmail.com留言,我会在24小时内与您联系!

相关文章
相关标签/搜索