Key用来干嘛
Flutter中的Key,一直都是作为一个可选参数在很多Widget中出现,那么它到底有什么用,它到底怎么用,本篇文章将带你从头到尾,好好理解下,Flutter中的Key。
我们首先来看下面这个Demo:
代码语言:javascript复制Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 100,
height: 100,
color: Colors.blue,
),
],
)
展示为两个不同颜色的方块。
问题1
这时候,如果我们在代码中交换两个Container的位置,Hot reload之后,它们的位置会发生改变吗?
下面我们把Demo修改一下,将Container抽取出来,并在中间放一个Text用来做计时器,并改为StatefulWidget,代码如下。
代码语言:javascript复制class KeyBox extends StatefulWidget {
final Color color;
KeyBox(this.color);
@override
_KeyBoxState createState() => _KeyBoxState();
}
class _KeyBoxState extends State<KeyBox> {
var counter = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: widget.color,
child: Center(
child: TextButton(
onPressed: () {
setState(() => counter );
},
child: Text(
counter.toString(),
style: const TextStyle(fontSize: 60),
),
),
),
);
}
}
代码语言:javascript复制Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KeyBox(Colors.yellow),
KeyBox(Colors.green),
],
)
这样当我们点击计时器工作之后,展示如下。
问题2
这时候,如果我们在代码中交换两个Container的位置,Hot reload之后,它们的数字会发生改变吗?
问题3
如果我们删掉第一个Widget,Hot reload之后,显示的是数字几?
问题4
如果我们再重新把删掉的Widget加回来,Hot reload之后,又会如何显示?
问题5
如果在问题2的基础上,给第一个Widget外新增一个Center,那么又会如何显示呢?
如果你能完全回答上面的这几个问题并知道为什么,那么恭喜你,看完这篇文章,你会浪费十几分钟,当然,如果你不清楚,那么这十几分钟的时间,将给你带来不小的收益。
Key是什么
Flutter通过Widget来渲染UI,那么它是如何区分上面的两个不同颜色的Container的呢?通过颜色吗?当然不是,如果Container的颜色相同,那岂不是无法区分了?
所以,Key就成了Flutter区分不同Widget的依据,这就好比是Android中布局的ViewID。
知道Key是什么还不够,我们还得知道,我们为什么需要Key,首先,我们来看下上面的三个问题。
对于问题1,这个应该很简单了,Container是StatelessWidget,所以每次Hot reload都会重新build,因此颜色肯定会发生互换,这个很好理解。
那么对于问题2呢?StatelessWidget改成了StatefulWidget,这次再交换两个Widget的位置,你可以发现,虽然颜色互换了,但是数字没变。
要怎么解决这个问题呢?这就需要用到Key了,我们给KeyBox增加一个Key的参数。
代码语言:javascript复制❝新的Flutter Lint已经会提示你构造函数需要增加key的可选参数了。 ❞
const KeyBox(this.color, {Key? key}) : super(key: key);
在使用的地方,传入ValueKey即可。
代码语言:javascript复制KeyBox(Colors.yellow, key: ValueKey(2)),
SizedBox(height: 20),
KeyBox(Colors.cyan, key: ValueKey(1)),
这时候你再切换两个Container的位置,数字就会跟着变换了。
Key的原理
Key实际上是Flutter用来标记Widget的唯一标识,但是为什么需要Key,就要从Flutter的渲染流程上说起了。
Widget作为Flutter中的不可变数据,是作为渲染的数据类而存在的,它实际上就是内容的配置表,根据View的树形结构,自然而然模拟出了一个Widget Tree的概念。
Widget在运行时会创建Element实例,这些Element和Widget也组成了一一对应的关系,对于StatefulWidget来说,Widget中包含了组件的外观、位置等信息,而Element中,包含了State信息,这也是Flutter的核心原理。所以,在上面的Demo中,Counter作为State,被保存在Element中,而颜色,被保存在Widget中。
Widget和Element分离之后,如果修改颜色等Widget属性,那么可以直接创建新的Widget替换旧的Widget,同时还可以保留Element中的数据,因为创建Widget的成本是很低的,而Element则会高很多,所以Element会持续尽可能长的时间。
那么在Widget被改变之后,Element是如何和Widget进行关联的呢?这就需要两个东西了:
- runtimeType
- Key
所以Element会先对比当前新的Widget Tree中的新元素,是否跟当前Element的类型一致,如果不一致,那么说明Element已经无效了,只能重新创建,如果类型一致,那么就需要进一步判断Key了。
问题2的原因
所以,在问题2中,由于两个Widget的类型并没有发生变化,而又没有Key,所以,Widget被重新创建后,与原来的Element又关联起来了,看上去就是只修改了颜色。
那么在问题2的解法中,我们给Widget增加了Key,当我们调换两个Widget的位置时,虽然类型没有改变,但是Key发生了改变,Element在原来的位置找不到对应的Widget,那么这时候,它会选择在当前层级下,继续搜索这个Key。
这里要注意,Element只会在当前层级下搜索,如果这个Key的Widget被移入了其它层级,那么也是无法找到的,在问题2的场景下,由于只是交换了两个Widget的顺序,所以Element会在后面找到之前Key的Widget,同理,下一个Element也会找到,所以,两个Widget都被关联起来了,所以State也显示正确了。
问题3的原因
那么在问题3中,我们删除了第一个Widget,当没有Key时,Element会在Widget Tree中搜索,当它发现第二个Key类型是一样的时,它就以为它找到了,而第二个Element,因为找不到Widget,就销毁了。最终的效果就是剩下第二个Box的颜色和第一个Box的数字。
那么如果有Key呢?有Key的话,就不会找错了啊,所以自然能够对应上,与我们预想的也就是一样的了。
问题4的原因
理解了问题3,那么问题4就好理解了。当我们在开头创建同一个类型的Widget时,Element会把这个新增的Widget当作是以前的Widget,因为它们类型相同,所以Element被关联到了这个新的Widget,而另一个Widget发现已经没有Element了,所以会选择新建一个Element,这时候,数字就是默认值0了。
问题5的原因
对于问题5来说,实际上就是Element的搜寻机制,前面解释了,Element只会在当前层级进行搜索,所以Center的加入,改变了Widget的层级,Element无法对应了,所以它也选择了消耗重建,所以第一个Widget会显示默认值0。
❝但是要注意的是,如果类型不一致,那么Flutter会直接判断不相同,从而直接消耗重建,所以,在这些问题里,如果在KeyBox之间插上一些不同类型的Widget,那么就瞬间破防了,演示的效果就完全不同了。 ❞
Key有哪些Key
Key从整体上来说,分为两种,即:
- Local Key:分为Value Key、Object Key和Unique Key
- Global Key
Local Key顾名思义,指的是在当前Widget层级下,有唯一的Key属性,而Global Key,则是在全局APP中,具有唯一性。Global Key的性能会比Local Key差很多。
Value Key
在前面的Demo中,我们给KeyBox增加了Key之后,Widget在修改、移动之后,Element就可以正确的找到对应的Widget了,这里我们使用的是Value Key。
Value Key,顾名思义,就是使用Value来对Key做标识的Key,例如我们在Demo中使用的,ValueKey(1),value可以是任意类型,这里是1,其实更符合的场景,应该是用Color,或者是更加具有语义性的value来作为Key的value。
Value Key在同一层级下需要具有唯一性,所以当两个KeyBox都设置成ValueKey(1)时,程序就会报错,告诉你Key重复了。
Object Key
Object Key与Value Key类似,但是又不完全一样,Value Key对比的是Value,Value相等,就是相等,而Object Key,对比的是实例,实例相同,才是相等,就好比一个Java中的equals,一个是「==」。我们看下Object Key的源码就一目了然了。
代码语言:javascript复制@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}
假如我们有一个自定义的Class,重写了它的==函数,那么用Value Key,new两个同样的对象,它们就是相等的,而Object Key,则不相等,原因就是一个比较的是值,一个比较的是引用。
Unique Key
Unique Key自己都说了,它是独一无二的,也就是说,Unique Key只和自己相等,任意创建多个Unique Key,都是不相等的,相当于唯一标识了。
如果在Build函数中创建Unique Key,那么这个Key在大部分场景下就没有意义,因为Hot reload时,Build函数会重建,所以Unique Key被重建,而且和之前也不相等。
这就很奇怪了,这玩意有什么用呢?
用处确实不多,但一旦用到,就必须得用,例如下面这个例子。
假如我们要用AnimatedSwitcher来实现切换时的动画效果,这时候,我们需要让每次改变都要执行动画,那么这里就可以使用Unique Key,强制每一次都是新的Widget,这样才能有动画效果。
那么另一种使用场景,就是在无法使用Value Key和Object Key的时候使用,但是这时候,需要将Unique Key定义在Build函数之外,这样Unique Key只会创建一次,从而保证唯一性的同时,不用去创建value和Object。
Global Key
Global Key全局唯一且只和自己相等,还记得之前Element在关联新变化的Widget时是怎么比较Key的吗——Element为了效率问题,只会在当前层级下进行寻找,所以,在问题5中,一旦我们修改了某个Widget的层级,那么Element就会消耗重建,那么如果使用了Global Key呢?当Key的类型是Global Key时,Element会不惜代价在全局寻找这个Key,这也是为什么Global Key的效率会比较低的原因。
那么有了Global Key,即使Widget Tree发生了改变,也依然可以找到这个Widget进行关联,但是要注意的是,Global Key需要定义在Build函数之外,否则每次都会重新创建Global Key,那就没有意义了。
除此之外,Global Key还有一个作用,那就是给一个Widget增加一个全局标识,这样有点像命令式编程的意思,类似Android中的FindViewByID,通过Global Key就可以找到当前标记的这个Widget,从而获取它的一些相关信息。
代码语言:javascript复制final count = (globalKey.currentState as _KeyBoxState).counter;
print('count: $count');
final color = (globalKey.currentWidget as KeyBox).color;
print('color: $color');
final size = (globalKey.currentContext?.findRenderObject() as RenderBox).size;
print('size: $size');
final position = (globalKey.currentContext?.findRenderObject() as RenderBox).localToGlobal(Offset.zero);
print('position: $position');
// output
flutter: count: 0
flutter: color: MaterialColor(primary value: Color(0xff4caf50))
flutter: size: Size(100.0, 100.0)
flutter: position: Offset(145.0, 473.5)
由此可见,通过Global Key,我们可以拿到State、Widget、Element(Context)以及通过Element关联的RenderObject,这样就可以获取Widget中的一些配置参数,State中的数据变量,以及RenderObject中的绘制信息,例如尺寸、位置、约束等等。
本文原创公众号:群英传,授权转载请联系微信,授权后,请在原创发表24小时后转载。