在开发中会发现每当我们创建一个小部件时,都有一个参数 key
,这个key到底有什么作用呢?下面就来介绍下key是什么,有什么作用以及如何使用。
key是什么
在 Flutter 开发中我们经常与要状态打交道。我们知道 Widget 有 Stateful
和 Stateless
两种。Key 能够帮助开发者在 Widget 树中保存状态,在一般的情况下,我们并不需要使用 Key。那究竟什么时候应该使用 Key 呢?
下面举个例子来说明:
- 创建了一个100*100的盒子,颜色是随机生成的,显示的文字是外面传进来的
class StfulItem extends StatefulWidget {
final String title;
const StfulItem(this.title, {Key? key}) : super(key: key);
@override
_StfulItemState createState() => _StfulItemState();
}
class _StfulItemState extends State<StfulItem> {
//随机生成颜色
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
child: Text(widget.title),
);
}
}
复制代码
- 创建了三个上面创建的盒子,然后点击按钮时删除第一个盒子
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
const StfulItem('1111'),
const StfulItem('2222'),
const StfulItem('3333'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("KeyDemo"),),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.delete),
onPressed: (){
setState(() {
items.removeAt(0);
});
},
),
);
}
}
复制代码
把 color 生成不放到 state
里面
class StfulItem extends StatefulWidget {
final String title;
StfulItem(this.title, {Key? key}) : super(key: key);
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
_StfulItemState createState() => _StfulItemState();
}
复制代码
发现效果也是一个个删除,并没有发现颜色复用,因为这个是更新了 widget 导致的。
把上面 StatefulWidget 改成 StatelessWidget,看下效果会是什么样?
class StlItem extends StatelessWidget {
final String title;
StlItem(this.title, {Key? key}) : super(key: key);
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(title),
color: color,
);
}
}
复制代码
发现这个时候删除后是正常的,没有复用前一个的颜色。
- 针对第一种情况,会复用之前的颜色,假如使用
value Key
会是什么效果呢?
List<Widget> items = [
StfulItem('1111', key: const ValueKey('1111')),
StfulItem('2222', key: const ValueKey('2222')),
StfulItem('3333', key: const ValueKey('3333')),
];
复制代码
发现确实达到了效果,没有在复用了,这里我就不贴运行的额效果了。
为什么 Stateful Widget
无法正常删除,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,我们会涉及 Widget 的 diff
更新机制。先介绍下 Flutter 的渲染原理
Flutter 的渲染原理
并不是所有的Widget都会被独立渲染!只有继承RenderObjectWidget的才会创建RenderObject对象。在Flutter渲染的流程中,有三颗重要的树!Flutter引擎是针对Render树进行渲染!
Widget:Widget里面存储了一个视图的配置信息,包括布局、属性等
。它是一份轻量的数据结构,在构建时是结构树,它不参与直接的绘制。
Element:Element是Widget的抽象,当一个Widget首次被创建的时候,那么这个Widget会通过Widget.createElement
,创建一个element,挂载到Element Tree遍历视图树。在attachRootWidget函数中,把 widget交给了 RenderObjectToWidgetAdapter这座桥梁,Element创建的同时还持有 Widget和 RenderObject的引用。构建系统通过遍历Element Tree来创建RenderObject,每一个Element都具有一个唯一的key,当触发视图更新时,只会更新标记
的需变化的Element。类似react中setState后虚拟dom树的更新。
RenderObject:在 RenderObject树中会发生 Layout、Paint
的绘制事件,大部分绘图性能优化发生在这里,RenderObject Tree构建为Canvas的所需描述数据,加入到Layer Tree中,最终在Flutter Engine
中进行视图合成并光栅化交给GPU。
Widget 更新机制
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
复制代码
其实知道 Flutter 的渲染原理后,我们知道 Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。当新的 Widget 到来时将会调用 canUpdate
方法,来确定这个 Element是否需要更新
。canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新 Widget 就可以了。
Stateless 的比较
我们在使用 StatelessWidget 时没有传入 key
,并且 runtimeType 也是一致的
,所以 canUpdate 返回 true。这个时候就需要 Widget 的 build 方法重新构建
,因为 color 和 title 都在这个里面,所以就会看到正常的删除效果。
Stateful 的比较
我定义的 color 是在 state
里面的,Widget 并不保存
State,真正 hold State 的引用的是 Stateful Element。 我们在使用 StatefulWidget 时没有传人 key ,并且 runtimeType 也是一致的,所以 canUpdate 返回 true,更新 widget,StatefulWidget 的 Element 将复用上一个的。
当我们传入一个 value key 之后,canUpdate 返回为 false。Flutter 的将会认为这个 Element 需要被替换。然后重新生成一个新的 Element 对象装载到 Element 树上替换掉之前的 Element。
几种类型的 key
key 本身是一个抽象类,有一个工厂构造方法创建ValueKey
-
直接子类主要有:LocalKey 和 GlobalKey
-
GlobalKey:帮助我们访问某个Widget的信息
-
LocalKey:它用来区别哪个Element要保留,哪个Element要删除。diff 算法核心所在
- ValueKey : 以值作为参数 (数字,字符串,任意类型)
- ObjectKey : 以对象作为参数
- LocalKey:(创建唯一标识)
LocalKey 的三种类型
LocalKey
继承自 Key, 翻译过来就是局部键,LocalKey
在具有相同父级的 Element
中必须是惟一的。也就是说,LocalKey 在同一层级中必须要有唯一性。
ValueKey
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ValueKey<T>
&& other.value == value;
}
复制代码
使用特定类型的值来标识自身的键,ValueKey 在最上面的例子中已经使用过了,他可以接收任何类型的一个对象来最为 key。
通过源码我们可以看到它重写了 == 运算符,在判断是否相等的时候首先判断了类型是否相等,然后再去判断 value 是否相等;
ObjectKey
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}
复制代码
ObjectKey 和 ValueKey 最大的区别就是比较的算不一样,其中首先也是比较的类型,然后就调用 indentical
方法进行比较,其比较的就是内存地址,相当于 java 中直接使用 == 进行比较。而 LocalKey 则相当于 java 中的 equals 方法用来比较值的;
UniqueKey
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}
复制代码
每次重新 build 的时候,UniqueKey 都是独一无二的,所以就会导致无法找到对应的 Element,状态就会丢失。有一种做法就是把 UniqueKey 定义在 build 的外面,这样就不会出现状态丢失的问题了。
GlobalKey
GlobalKey
继承自 Key,相比与 LocalKey,他的作用域是全局的,而 LocalKey 只作用于当前层级
。 整个应用程序里都是唯一的,所以同一个 GlobalKey 只能作用在一个widget上。可以通过 GlobalKey拿到所对应的 state 和 element 或者 widget ,用以改变 state 的状态或者变量值。
代码示例:
通过点击悬浮按钮修改内容的值
class GlobalKeyDemo extends StatelessWidget {
GlobalKeyDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GlobalKeyDemo'),
),
body: ChildPage(),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
},
),
);
}
}
class ChildPage extends StatefulWidget {
const ChildPage({Key? key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(count.toString()),
Text(data),
],
),
);
}
}
复制代码
我们会发现是不在同一个 widget 里面的,无法修改 ChildPage 中 text 文本的内容,这个时候就需要用到 GlobalKey 来实现
class GlobalKeyDemo extends StatelessWidget {
//定义全局的key
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
GlobalKeyDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GlobalKeyDemo'),
),
//把全局的key传到其他部件里面去
body: ChildPage(key: _globalKey,),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
_globalKey.currentState!.setState(() {
_globalKey.currentState!.data = 'old:' + _globalKey.currentState!.count.toString();
_globalKey.currentState!.count++;
});
},
),
);
}
}
class ChildPage extends StatefulWidget {
const ChildPage({Key? key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(count.toString()),
Text(data),
],
),
);
}
}
复制代码
通过 Global Key 我们获取内容有以下API:
- currentContext: 可以找到包括renderBox在内的各种element有关的东西
- currentWidget: 可以得到widget的属性
- currentState: 可以得到state里面的变量.
Global Key的实现原理
在 GlobalKey 内部有一个静态的_registry Map集合,该集合以 GlobalKey 为 key,以 Element 为Value 。其提供的 currentSate 方法就是以 GlobalKey 对象为 key 获取对应的 Element 对象,然后就可以通过 Element.state 获取具体的值。
我不知道为什么多文章介绍 GlobalKey
是非常昂贵的,没有特别的复用需求,不建议使用它,通过打断点发现是会调用 unmount
-> _unregisterGlobalKey
进行销毁的,为什么还说昂贵呢!
实现效果:
小结
当你想要跨widget
树保留状态时,应该使用key
。当修改相同类型的widget
集合时,,要将key
放在要保留的widget
的树的顶部。LocalKey 在同一层级中必须要有唯一性,GlobalKey 的作用域是全局的。
-
GlobalKey:帮助我们访问某个Widget的信息
-
LocalKey:它用来区别哪个Element要保留,哪个Element要删除。
- ValueKey:以值作为参数 (数字,字符串,任意类型)
- ObjectKey:以对象作为参数
- LocalKey:(创建唯一标识)
参考文章:
Flutter的渲染原理:zhuanlan.zhihu.com/p/135969091
Flutter | 深入浅出Key:juejin.cn/post/684490…