Flutter Key的原理和使用 (二) Widget 和 Element 的对应关系

1,344 阅读4分钟

这是我参与8月更文挑战的28天,活动详情查看:8月更文挑战

Flutter Key的原理和使用 (一) 没有Key会发生什么 (juejin.cn)

Flutter Key的原理和使用 (二) Widget 和 Element 的对应关系 (juejin.cn)

Flutter Key的原理和使用(三) LocalKey的三种类型 (juejin.cn)

Flutter Key的原理和使用(四) GlobalKey 的用法 - 掘金 (juejin.cn)

Flutter Key的原理和使用(五) 需要key的实例:可拖动改变顺序的Listview - 掘金 (juejin.cn)

上一篇文章中,我们遇到了一个问题 , widget换位置的时候,出现了一些意外,并没有按照我们所想去调换位置, 这时候我们就很困惑, 那是在我们看来,写了一个widget,那这个widget就该按照我们设定的那样展示, 但实际上, widget并不是最终出现在屏幕上的东西.

Widget Tree

可能有人听说过Widget Tree , 就是嵌套着widget的东西. 比如现在这段代码:

Column(
  children: [
    Box(Colors.blue),
    Box(Colors.red),
  ],
)

对应的Widget Tree就是下面这个样子:

 - Column 
   - Box
   - Box

但真正绘制在屏幕上的并不是这个Widget Tree, 我们都知道Wdiget是描述UI元素Element的配置,所以实际运行起来的时候,还会产生一个Element Tree, Element Tree就是Widget Tree实例化之后产生的对象.

Widget Tree ---实例化---> Element Tree

 - Column                             - ColumnElement
   - Box             -实例化->          - BoxElement
   - Box                                - BoxElement

Widget负责如何渲染,比如颜色,大小,形状等等.

Element负责管理里面的状态.

所以状态是随着Element来改变的,外观是随着Widget改变的.二者是分开的.

为啥要分开的呢?

因为Widget是不可变的,我们可能经常会看到这样的提示:

image.png

不可变,意思就是创建之后就不能在运行时改变它,但是State是可以变的.通常我们会用setState通知flutter去重建一个新的widget,而不是去改变这个widget,因为widget是不可变的.但是丢掉这个widget,并没有把状态也一起丢掉.

因为大部分UI的设计,是通过我们改变Widget来实现的,所以如果能把Widget和State分开,在hot reload的时候,就能在改动widget的时候保留它的状态. 如果widget每次有变化,state就丢掉的话,程序就没法用了,没有办法支持Hot Reload和动画效果了.

为什么改变Widget的顺序,UI上并没有改变顺序

所以当Wdiget改变的时候,我们尽量将它与原来的State关联起来.

如何关联的呢?

当Widget改变的时候,flutter会去看Wdiget和这个Element是不是同样的的,比如我们把Columen换成Row,那它们就不一样了,然后会去看Key是一样,这也就是Key可以帮助Flutter把容易混淆的Widget区分清楚.当然,如果不传key,它就没办法对比key了,如果widget不变,依然会使用之前的element.

有点类似判断两个对象是否相等,会先去比较它们的类型.

因此我们把Box调换位置的时候,比如上面有两个Box, 改变位置的时候,对应的Widget Tree是原来的类型,所以BoxElement刚好可以和之前的Box对应上,所以Flutter会以为没有改变.

所以当我们做如下改变的时候:

Column(                                 Column(
  children: [                              children: [
    Box(Colors.blue),         ->             Box(Colors.red),
    Box(Colors.red),                         Box(Colors.blue),
  ]                                         ]  

在Flutter看来,就是将上面的Widget变成了红色, 下面的变成了蓝色而已. 这么看似乎也没有问题.

接下来,我们看看有Key的情况下

 - Column                             - ColumnElement
   - Box (Key1)            -实例化->              - BoxElement (Key1)   
   - Box (Key2)                                   - BoxElement (Key2)   

将上下两个Box调换位置, 在比较类型相同之后比较Key的时候,第一个BoxElement (Key1)发现对应位置的Box的key不一致,此时Flutter就会在同一级别去搜索其他widget,看看有没有一样的key,然后找到了下面有一个对应的box,同理下面的也是一样的找法.

那么改变之后的样子就是这样的:

 - Column                                      - ColumnElement
   - Box (Key2)            -实例化->              - BoxElement (Key1)   
   - Box (Key1)                                   - BoxElement (Key2)   

也就达到了状态也会互换的作用,Flutter能够借助Key来跟踪顺序的变化.

那如果我们像之前一样删除其中一个呢?

Column(
  children: [
    Box(Colors.blue),
    Box(Colors.red),
  ],
)

删掉第一个之后变成:

Column(
  children: [
    Box(Colors.red),
  ],
)

那么红盒子就变成了第一个. 原来的

 - ColumnElement
   - BoxElement
   - BoxElement

第一个BoxElement就对应了这个红色的Box,第二个没找到,此时它就被丢弃了.

那有Key的情况下,第一个Element去找Widget的时候,第一个key对不上,同级又找不到对应的,那么它自己就丢弃了,第二个Element就对应到了第一个,变化也就和我们预期的一致了.

增加一个Wdiget也是同样的道理.因为Element是按顺序从上到下找的,没有key的情况下,新增的同类型widget,会替代之前位置上的widget与element对应,最后位置的widget因为没有对应的element,它就会调用createState来创建一个新的Stete.