【Flutter】Widget的key是干啥的

前言

以前入门一些Flutter应用的时候,老是会遇到GlobalKey这个类,当时我只从代码的语法上感知到这个东西确定是用来绑定某些东西的,但至于key这东西是啥?为何要绑定?不绑定的话会怎么样?为何有的Widget实现须要绑定有有的不须要?这些通通都不知道。html

因而趁着端午有时间,就认真翻了下官方文档,发现官方文档说得很是详细(前提是你对Flutter的控件树有必定理解),上面的问题基本都回答到,惋惜的是官方是用视频(YouToBe)讲解的,这不便于忘记的时候速读翻阅,因而我就整理成这篇博客顺便加固下印象。java


key是什么

key的做用是:控制weidget树上的widget是否被替换(刷新)git

若是两个weidget的runtimeTypekey属性相等(用==比较),那么本来指向旧weidge的element,它的指针会指向新的widget上(经过Element.update方法)。若是不相等,那么旧element会从树上移除,根据当前新的widget从新构建新element,并加到树上指向新widget。github

咱们能够看下代码是否是这么回事: Element.updateapi

@mustCallSuper
  void update(covariant Widget newWidget) {
    // This code is hot when hot reloading, so we try to
    // only call _AssertionError._evaluateAssertion once.
    assert(_lifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && Widget.canUpdate(widget, newWidget));
    // This Element was told to update and we can now release all the global key
    // reservations of forgotten children. We cannot do this earlier because the
    // forgotten children still represent global key duplications if the element
    // never updates (the forgotten children are not removed from the tree
    // until the call to update happens)
    assert(() {
      _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
      _debugForgottenChildrenWithGlobalKey.clear();
      return true;
    }());
    _widget = newWidget;
  }
复制代码

进入上面的Widget.canUpdatemarkdown

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

能够看到判断逻辑基本与文档一致,这里有个值得注意的是:Widget自己不会调用Widget.canUpdate,这个方法是由Element负责调用的,也就是Widget能不能更新,最终仍是Element说了算app

相等时.png

不相等时.png

相信看到这里你已经明白key是啥以及它的做用了,but talk is cheap show me the code,那么咱们怎么证实这理论是对的呢?下面就给出了代码demo。less


何时会用到key

建一个demo先

下面先举一个不须要用key的例子,代码逻辑是,集合的元素顺序变动后,控件要跟着变化,代码以下:dom

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(home: PositionedTiles()));
}

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  List<Widget> tiles;

  @override
  void initState() {
    super.initState();
    tiles = [
      // StatefulColorfulTile(),
      // StatefulColorfulTile(),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatefulColorfulTile(key: UniqueKey()),
      StatelessColorfulTile(),
      StatelessColorfulTile(),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Row(
          children: tiles,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        // child: Icon(Icons.sentiment_very_dissatisfied),
        onPressed: swapTiles,
      ),
    );
  }

  void swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}

// ignore: must_be_immutable
class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
        color: color,
        child: Padding(padding: EdgeInsets.all(70.0))
    );
  }
}

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

  @override
  State<StatefulWidget> createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  Color color;

  @override
  void initState() {
    super.initState();
    color = ColorUtil.randomColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: color,
        child: Padding(padding: EdgeInsets.all(70.0))
    );
  }
}

class ColorUtil {
  static Color randomColor() {
    var red = Random.secure().nextInt(255);
    var greed = Random.secure().nextInt(255);
    var blue = Random.secure().nextInt(255);
    return Color.fromARGB(255, red, greed, blue);
  }
}
复制代码

上面的代码效果以下,能够看到使用StatelessColorfulTile时,点击按钮后两个色块能成功交换: QQ20210613173911HD.gifide


接下来咱们把代码改为下面这样煮,重启:

@override
  void initState() {
    super.initState();
    tiles = [
      StatefulColorfulTile(),
      StatefulColorfulTile(),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatefulColorfulTile(key: UniqueKey()),
      // StatelessColorfulTile(),
      // StatelessColorfulTile(),
    ];
  }
复制代码

神奇的事情发生了,点击按钮后,色块再也不发生交换: QQ20210613174103HD.gif

那在使用StatefulColorfulTile的前提下,如何让色块再次点击按钮后能发生交换呢?我猜聪明的你已经想到了,就是设置key属性,即把代码改为下面这个样子,重启:

@override
  void initState() {
    super.initState();
    tiles = [
      // StatefulColorfulTile(),
      // StatefulColorfulTile(),
      StatefulColorfulTile(key: UniqueKey()),
      StatefulColorfulTile(key: UniqueKey()),
      // StatelessColorfulTile(),
      // StatelessColorfulTile(),
    ];
  }
复制代码

效果以下:

QQ20210613172343HD.gif

接下来就是图解形成这些效果的缘由了。


为啥StatelessColorfulTile能交换

咱们先来看看StatelessColorfulTile交换的时候都发生了什么,先来看看交换前的:

image.png

交换后的:

image.png

当代码调用PositionedTiles.setState交换两个Widget后,flutter会从上到下逐一对比Widget树和Element树中的每一个节点,若是发现节点的runtimeType和key一致的话(这里没有key,所以只对比runtimeType),那么就认为该Element仍然是有效的,可用复用,因而只须要更改Element的指针,就能够直接复用。

而因为StatefulColorfulTile的颜色信息是存储在widget中的:

class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();
  
   ...(略)
}
复制代码

因此即使色块Widget由于Widget.canUpdate返回不须要更新,内部没有回调到setState逻辑,也会成功交换。

Element保存了Widget和RenderObject,Widget是负责描述控件样式,RenderObject则是布局渲染控制,当Element只更新了Widget,下一次渲染时就会变成新Widget的效果了。


为啥StatefulColorfulTile要加key才能交换

先从代码的最表面说说StatefulColorfulTileStatelessColorfulTile一个重大的区别,即Color的属性放的位置不同。

StatelessColorfulTile的Color属性是直接放置在Widget下的:

class StatelessColorfulTile extends StatelessWidget {
  Color color = ColorUtil.randomColor();
  
   ...(略)
}
复制代码

StatefulColorfulTile的Color属性是放在State下的: image.png

这里补充一个基础知识,即State属性,最终都会被Element管理,下面能够简单追几段源码看看。

首先看看StateFulWidget的抽象方法: image.png

有了Flutter三棵树概念之后,咱们应该明白每一个Widget最终都会被建立出对应的Element,而建立的方法正是上面的createElement,它会调用StatefulElement构造函数来构造。

接着跟进StatefulElement()函数,咱们就能清晰地看到StatefulElement管理了State,而且拿它来作各类各样的事了: image.png

明确了State属性,最终都会被Element管理这个大前提后,接下来就好办了。


咱们先来看看StatefulColorfulTile不带key的时候,调用交换函数究竟发生了什么,依旧是先看交换前的: image.png

交换后的: image.png

相信缘由不用我多说了,首先仍是Widget更新后,flutter会根据runtimeTypekey比较Widget从而判断是否须要从新构建Element,这里key为空,只比较runtimeType,比较结果必然相等,因此Element直接复用。

StatefulColorfulTile在从新渲染时,Color属性再也不是从Widget对象(即自身)里获取,而是从ElementState里面获取,而Element根本没发生变化,因此取到的Color也没有变化,最终就算怎么渲染,颜色都是不变的,视觉效果上也就是两个色块没有交换了。


接着看有了key以后,交换前:

image.png

交换后,发现两边key不相等,因而尝试匹配Element是否还有相同的id,发现有,因而从新排列Element让相同key的配对: image.png

若是Element这边没有key能与新Widget匹配得上,那么旧的Element会失效,后续根据新Widget从新构建一个Element。

rebuild后,Element已改变,从新渲染后视觉上就看到两个色块交换位置了: image.png

熟悉三棵树原理的咱们知道,Element就至关于设备上的真实控件,既然Element的位置变化了,那么最终屏幕上的控件也就跟着变化了,最终交换后从新渲染给视觉上就是两个色块交换了。

好了,本篇博客先到这里结束了,这里只是简单介绍了下Widget中key的做用,但实际上Key还有不少种实现,他们用处各有不一样,这个由于和本篇目标没啥太大关系,因此不介绍了,有空本身翻翻官方文档其实很快也能搞懂了。

相关文章
相关标签/搜索