Flutter之Key的原理解析
前言
在Flutter开发中,会发现在很多的组件的构造函数中都会有一个可选的参数Key
,你可能会疑惑这个Key的作用是什么?这篇文章就来揭开Key的面纱。
1、案例解析
先不着急去看Key
是什么?我们先看一下下面这段代码的运行。
class _KeyDemoState extends State<KeyDemo> {
List<Widget> itemList = [
StatelessItem("A"),
StatelessItem("B"),
StatelessItem("C"),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("KeyDemo"),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: itemList,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
itemList.removeAt(0);
});
},
),
);
}
}
class StatelessItem extends StatelessWidget {
final String _title;
StatelessItem(this._title);
final _color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: _color,
child: Text(_title),
alignment: Alignment.center,
);
}
}
其实这段代码很简单,就是在界面上显示三个不同颜色的Container
,点击按钮依次删除第一个色块。
如上所示结果正如我们所诉求的一样,并无不妥,那么如果我们将StatelessItem
替换成StatefulWidget
的widget又当如何呢?
class StatefulItem extends StatefulWidget {
final String _title;
StatefulItem(this._title);
@override
_StatefulItemState createState() => _StatefulItemState();
}
class _StatefulItemState extends State<StatefulItem> {
final _color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 100);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: _color,
child: Text(widget._title),
alignment: Alignment.center,
);
}
}
正如上所示的效果,在操作的过程中出现了错乱,本应该已删除的第一个色块的颜色显示在了第二个色块的部分。那么为什么会出现这样的现象?这和Flutter的Widget
的diff
更新机制有莫大的关系。
2、Widget更新
如果你对于Flutter有一定了解,那么一定知道Flutter中的三棵树Widget Tree
,Element Tree
,Render Tree
共同铸就Flutter的渲染流程,关于Flutter的渲染流程会在后续的文章中详细介绍。
在Flutter中使用Widget来构建你的UI,Widget描述了他们的视图在给定其当前配置和状态时的样式,当widget的状态发生变化时,widget会重新构建UI。看起来重新构建UI的过程是重新渲染Widget树的过程,实际上Widget只是想当于一个配置清单,Element才是被使用的对象,重新渲染视图是对Element树的渲染过程。为了性能上的考虑,重新渲染的先决条件是判断两个新老widget的runtimeType
和key
是否一致。如果一致则说明不需要替换Element,直接更新widget就可以了。
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
2.1、StatelessItem的比较过程
在第一个代码里面的Container是一个StatelessWidget
的widget,同时也没有传入key
到构造函数中,所以对于canUpdate
方法而言,只需要比较新老widget的runtimeType
即可,很明显这里的runtimeType是一致的,所以会返回true
。当点击按钮删除第一个Container时,StatelessElement
便会调用新持有Widget的build
方法重新构建UI,所以便可以看到色块是按顺序删除的。
2.2、StatefulItem的比较过程
同理在StatefulWidget
的Container中,也没有传入key
,所以对于canUpdate
方法而言,只需要比较新老widget的runtimeType
即可,很明显这里的runtimeType是一致的,所以会返回true
。而不同的是颜色是在State
中的,在点击按钮删除一个Container时,会重新build
构建UI,但是不会删除Element
,只是从原持有的State
实例中build新的widget。因为Element没有变,所以State不会变化,那么颜色也不会变化。
而如果给Widget一个key
之后,canUpdate
方法将会返回false
,即表示需要重新构建Element。此时RenderObjectElement
会用新Widget的key在老Element列表里面查找,找到匹配的则会更新Element的位置并更新对应renderObject
的位置,对于这里的代码而言就是删除对应的Element。
整个过程如上图所示,原本的Wiget和Element之间的关系如黑色连线所示,待删除Widget A后,它们之间的关系便会如红色连线所示。
传入key后的效果如下:
List<Widget> itemList = [
StatefulItem("A", key: ValueKey(1)),
StatefulItem("B", key: ValueKey(2)),
StatefulItem("C", key: ValueKey(3)),
];
3、Key的分类
abstract class Key {
const factory Key(String value) = ValueKey<String>;
@protected
const Key.empty();
}
Key
本身是一个抽象类,由此派生出两种不同用途的Key:LocalKey
和GlobalKey
。
3.1、LocalKey
LocalKey
直接继承自Key,是用作Widget刷新的diff算法的核心所在,用作Element和Widget进行比较。
LocalKey又派生出很多的子类:
ValueKey
:以一个数据作为Key。如:数字、字符等;ObjectKey
:以Object对象作为Key;UniqueKey
:可以保证Key的唯一性;(一旦使用Uniquekey那么就不存在Element复用 了!)
3.1、GlobalKey
GlobalKey
可以用来标识唯一子widget。GlobalKey在整个widget结构中必须是全局唯一的,而不像LocalKey只需要在兄弟widget中唯一。由于它们是全局唯一的,因此可以使用GlobalKey来获取到对应的Widget的State对象!
转载自:https://juejin.cn/post/6948686301088448549