最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【Flutter】Widget的key是干啥的

    正文概述 掘金(eee)   2021-06-15   741

    前言

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

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


    key是什么

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

    如果两个weidget的runtimeType和key属性相等(用==比较),那么原本指向旧weidge的element,它的指针会指向新的widget上(通过Element.update方法)。如果不相等,那么旧element会从树上移除,根据当前新的widget重新构建新element,并加到树上指向新widget。

    我们可以看下代码是不是这么回事: Element.update

      @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.canUpdate

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

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

    【Flutter】Widget的key是干啥的

    【Flutter】Widget的key是干啥的

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


    什么时候会用到key

    建一个demo先

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

    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时,点击按钮后两个色块能成功交换: 【Flutter】Widget的key是干啥的


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

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

    神奇的事情发生了,点击按钮后,色块不再发生交换: 【Flutter】Widget的key是干啥的

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

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

    效果如下:

    【Flutter】Widget的key是干啥的

    接下来就是图解造成这些效果的原因了。


    为啥StatelessColorfulTile能交换

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

    【Flutter】Widget的key是干啥的

    交换后的:

    【Flutter】Widget的key是干啥的

    当代码调用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逻辑,也会成功交换。


    为啥StatefulColorfulTile要加key才能交换

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

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

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

    StatefulColorfulTile的Color属性是放在State下的: 【Flutter】Widget的key是干啥的

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

    首先看看StateFulWidget的抽象方法: 【Flutter】Widget的key是干啥的

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

    接着跟进StatefulElement()函数,我们就能清晰地看到StatefulElement管理了State,并且拿它来做各种各样的事了: 【Flutter】Widget的key是干啥的

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


    我们先来看看StatefulColorfulTile不带key的时候,调用交换函数究竟发生了什么,依旧是先看交换前的: 【Flutter】Widget的key是干啥的

    交换后的: 【Flutter】Widget的key是干啥的

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

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


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

    【Flutter】Widget的key是干啥的

    交换后,发现两边key不相等,于是尝试匹配Element是否还有相同的id,发现有,于是重新排列Element让相同key的配对: 【Flutter】Widget的key是干啥的

    rebuild后,Element已改变,重新渲染后视觉上就看到两个色块交换位置了: 【Flutter】Widget的key是干啥的

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

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


    起源地下载网 » 【Flutter】Widget的key是干啥的

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元