每一个Widget都有一个可选传递的参数key,我们一般不会使用到它,但是在一些特定或者复杂的场景下,它必须出场,扮演着重要的角色,我们一起来学习认识一下它.
一.没有Key会发生什么事情
我们创建一个具有状态的Widget(Box)
class Box extends StatefulWidget {
final Color? color;
Box(this.color, {Key? key}) : super(key: key);
@override
_BoxState createState() => _BoxState();
}
class _BoxState extends State<Box> {
int count = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
setState(() {
count++;
});
},
child: Container(
width: 150,
height: 150,
color: widget.color,
child: Center(
child: Text('$count',style: const TextStyle(fontSize: 50),),
)),
);
}
}
大家可以看到,里面的数字是在state里面,我们手指点击数字就会加一。 然后现在看看主程序的代码:
class _KeyShareDemoState extends State<KeyShareDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title ?? ''),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Box(Colors.red),
Box(Colors.yellow),
Box(Colors.blue),
],
),
),
);
}
}
初始状态:
我们放在同一层级的Column下面,然后第一个Box点击一下,第二个Box点击两下,第三个Box点击三下,运行结果如下图
颜色顺序为红、黄、蓝,数字分别为1、2、3.
情景一
现在,我们人为的将第一个Widget和第三个Widget的代码顺序进行对调,我们猜想:此时的颜色和数字都会进行对调,是吗?结果如下图:
我们发现:颜色是对调了,但是里面的数字还是原来的数字,令人费解.
情景二:
我们回到初始化的状态,还是红、黄、蓝的三个按钮,数字还是1、2、3,我们现在人为的去掉第一个红色的Widget,那么我们猜想是不是应该留下数字2和3,数字1 不见了,颜色剩下黄色和蓝色呢?运行结果如下.
我们发现,颜色每次都是对的,没有让我们失望,但是这个数字1居然留下了,数字3不见了,我们更加疑惑了.
这个时候系统其实已经分不清楚谁是谁了,因为它们处于同一层级,又是相同的类型。所以,我们需要给它们一个Key作为它们各自的唯一标识符.如下图:
我们给了它们每个人一个ValueKey,并且每一个都给了不同的值,我们就发现,无论我们是调换顺序也好,还是删除其中一个Widget也好,它们都会如我们所愿,不会乱了.
结论:在同一层级树中(注意不同层级另当别论,这里的同一层级指的是三个Box都在同一个Column下面),Key可以作为唯一标识,标记我们的Widget,系统在复用的时候才不会混乱.
情景三
我们现在有Key了,都是ValueKey.我在第三个Box外面套上一层Center这个小部件,依然保留ValueKey.如下图:
这样加上了Center过后,然后点击热更新,事情再次没有如我们所愿,我们的第三个Box,刚开始是3,现在变成了0.但是颜色依然没有问题!
情景四
我这里就不进行演示了,大家可以去进行尝试,在同一层级下的Container和Text,你调换位置和删除,都会如你所愿,不会混乱。因为它们都是StatelessWidget,他们没有状态state.我们在几次的试验中也发现,颜色处于widget侧,它始终是没有问题的,出问题的是状态。
二.Widget树与Element树的对应关系
为了解释上面的奇异现象,我们必须去了解与学习Widget树与Element树的原理与关系。如下图所示:
1.Widget树:我们平时写代码的地方,它是一个蓝图,是一份描述UI元素Element的配置的描述文件.
2.Element树:真正生成视图对象和状态的树。它与widget是一一对应的关系,每个widget都会调用其内部的 createElement方法,从而实例化生成Element对象。如下图,为Widget的源码.
3.State是跟着Element对象走的,并不在Widget侧。Widget负责如何渲染,比如颜色,大小,形状等等,而Element负责管理里面的状态。所以状态是随着Element
来改变的,外观是随着Widget
改变的.二者是分开的。
4.Widget和Element为什么要分开呢? 因为Widget是不可变的!可以改变的是State状态,当状态发生改变后,flutter会去重建一个新的widget,去替换掉旧的widget,而不是去改变这个widget,因为widget是不可变的.
5.我们在上面的例子中,交换两个Box的位置,或者去掉一个Box时,它们所对应的Element并不一定被调换了顺序,或者说被正确的删除了。这才是问题的关键所在.如图所示:
这是一个Widget里面的源码方法,canUpdate方法会对新旧widget的类型和key进行判断,如果它们都相等,系统就认为它们可以更新。
意思是:一旦这个方法判定成立,Widget与Element对象就可以进行对应!可以进行关联!那么一旦判断错误,该Widget就会错误的与Element和State进行关联。上面我举出的例子,就是发生了这种情况!
三.复盘解释上面的现象
1.widget顺序交换问题.
无key:
当我们调换顺序时,系统重走canUpdate方法进行判定,由于在同一层级下,全部都是Box类型的Widget,并且我们没有给到key,key都为null,null==null。那么,此判断条件就成立了,返回true。也就是说,以前的Element1就关联上了交换顺序后的widget3,由于Element1的状态为1,所以第一个方块还是显示的1.同理可得,由于我们没有给Key,以前的Element3就关联上了交换顺序后的widget1,Element3的状态为3,那么第三个方块还是显示的3.由此可见,虽然我们交换了widget的顺序,但是Element的顺序并不一定会跟着发生改变,一切以canUpdate判定结果为准.
有key:
比如现在我们交换的是Box2与Box3的顺序,系统在检索Box3与Element2的时候,由于我们给了Key,那么canUpdate方法将返回false,他们将不会建立联系,如图所示:
不能建立对应联系的话,它会继续进行同级别检索(注意这里我还是说的同级,跨级我后面会说明),同级检索到可以建立对应联系的Box2后,它们就会建立正确的对应关系了,如图:
到了最后,每个widget与Element都建立了正确的对应关系,那么对应的状态也跟着走了,数字也正确了,这样也就达到了我们的目的.如图所示:
2.例子中删除widget的问题
有了上面的详细解释,这个由一张图概括:
我们删除掉第一个widget后,由于我们没有给Key,根据canUpdate方法的判定,Box2会与之前的Element1对应,Box3与之前的Element2对应,所以数字1和2被保留。而Element3由于在同级中没有检索到能与之匹配的Widget,那么Element3对象和State都会一并没销毁.
3.例子中有给Key,套上Center导致的问题.
可能你也发现了,开始的类型是Box,我们虽然给了Key,但是我们套上Center过后,这个类型就变了。canUpdate类型判断就不会成功。那么ELement3由于在同层级无法对应,就会被销毁,系统会重新生成新的与Center对应的Element对象,那么状态也被重置了。所以数字就是初始化的状态0.
4.StatelessWidget不需要使用Key?
我们开发中常见的Container、Text都是StatelessWidget,它是没有状态State的。比如在一个Column中,我们写两个Container,我们不传入key。现在我们交换他们的位置,系统会只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,两个 Widget 被交换了位置,但是此时这两个 Element 将不会交换位置,Element调用新持有Widget的build方法重新构建,所以widget得到更改,位置交换。UI的内容,都在widget侧,所以在同一层级树中,不需要使用Key.
三.几种Key的介绍使用.
我们主要有两大类的Key需要了解.分为局部键与全局键。
- LocalKey 局部键,在同一级中要唯一,可以理解为
同级唯一性
- GlobalKey 全局键 , 在整个App中必须是唯一的.
局部键的性能是比全局键快的多,因为它只是在同级中搜索比较,全局键在整个工程中都是唯一的,所以它更慢,当然它更加的强大.
局部键:
刚刚我们演示的代码都在一个Colunm之中,他们处于同一层级,那么我们就使用的局部键中的ValueKey。
同一层级中,键必须是唯一的,不然系统就会报错崩溃。比如我们的Column、Row、Stack、TabarView,它们的子部件都是同一层级,如果程序出现异常,你就要进行思考是否要使用Key了。
同一层级,使用相同类型的Widget,这个时候子Widget的状态数据需要更新(比如搜索功能),这是Key的一个实战使用的场景
1.ValueKey : 我们可以看到,里面是一个范型.我们传什么都可以,但是注意,它比较的是值!它的使用,我们在上面的例子中已经展示.
2.ObjectKey : 它和ValueKey的区别就在于,它比较的是对象,而不是值.如图:
可以看到它比较的是Object了,不再是一个范型了。这是这两个Key唯一的区别.
那么我们现在定义一个叫做People的对象出来,如下所示.
class People{
final String name;
final int age;
People(this.name, this.age);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is People && runtimeType == other.runtimeType && name == other.name && age == other.age;
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
我现在把它用到ValueKey上面,我故意把它们的值定义为相同的,这样子系统就可以报错出来,因为同一层级不能使用相同的Key。如图所示,系统果然在提示我说,你使用了重复的Key,那么这两个ValueKey就是想等的.
我现在使用ObjectKey,程序运行正常,没有报错,系统不认为它们两相等,因为它在比较两个对象是否相等.
3.UniqueKey
总结:我们前面提到的ValueKey和ObjectKey,还有我们后面即将介绍的GlobalKey,它们有一个共同点就是,它们会帮助我们保持状态State,与其说是保持状态,还不如说是保持了Element对象(因为State是跟着Element走的嘛),因为这些Key自己不会变,所以在canUpdate方法判定中,能够判断成功,判定成功的话,Element就能够与Widget对应上来,所以状态State就被保留下来。
但是UniqueKey不一样,它是与自身进行比较,并且它每一次都不一样,它自己会变.每一次的UniqueKey()和UniqueKey()它是不相等的。每一次刷新页面,它都是独一无二的,那么canUpdate方法永远都不会判定成功,widget与Element永远不会对上,Element对象每次都会被重新创建,与之一体的State也一同被重新构建,状态会被重置!每次都会被重置!换句话说就是:我们在刷新页面的时候,UniqueKey主动为我们丢失了状态, 让状态回到原点.
4.GlobalKey
1.全局键,它依然可以为我们保持状态State,与局部键不同的是,它可以为我们跨越层级的保持widget的状态。因为它是在整个App的树中进行索引查找的,所以它的速度更慢,但是也正因如此,可以实现跨层级保持ELement.
final GlobalKey _globalKey= GlobalKey();//不同其他的Key,它需要进行初始化被持有.
2.GlobalKey为我们提供了几个主要的方法,让我们既可以访问到Widget树的东西,也可以访问到Render树的东西,当然也可以访问到Element树的东西.
2.1 获取对应的widget
onPressed: () {
final widget = _globalKey.currentWidget as Box;
print(widget.color);
}
此处的Widget类型是Box类型,所以需要转换一下,我们当时在Box的Widget侧定义了一个color属性,我们就可以通过这种方式访问到它.
2.2 获取对应的context和RenderObject
context其实就是Element呀,通过它也可以访问到RenderObject。
final renderBox = _globalKey.currentContext!.findRenderObject() as RenderBox;
我们通过RenderBox就可以获取到它的尺寸和坐标了.
2.3 获取对应的State状态
我们定义一个GlobalKey,我们顺带可以给上它需要定位的state状态类型为_BoxState类型。
如图所示,我们点击三次过后,状态数字为3.可以通过GlobalKey获取到_BoxState,并打印出该count值
四.什么时候会使用Key?业务场景是什么
- 搜索功能 : 可能用到,当搜索不通内容的时候,给出不同的结果,你可能需要改变状态,重新构建Widget等, 这个需要看你的需求和实现方式.
- ValueKey:对列表ListView中item进行滑动删除、调换顺序、增加等的时候需要用到。
- ObjectKey:如果你有一个电话本应用,它可以记录某个人的电话号码,并用列表显示出来,同样的还是需要有一个滑动删除操作.我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和电话号码组合起来的 Object 将具有唯一性
好了,以上!