key有啥用?
绑定widget和element,在widget移动的时候,带上element一起移动。
源码
key源码解释:
///class Key
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// {@youtube 560 315 <https://www.youtube.com/watch?v=kn0EOS-ZiIc>}
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also:
///
/// * [Widget.key], which discusses how widgets use keys.
机翻:
key是widget、element、semanticsNode的标识。如果一个key用于绑定一个element和一个widget,那么新的widget只会被用于更新这个已经存在的element。
既然注释这么写了,是不是,不使用key的话,widget和element,就不是绑定关系?
好像,还真的是这样。不绑定,就不会存在关系!(废话
Widget中对于key的源码解释:
/// class Widget
/// Controls how one widget replaces another widget in the tree.
///
/// If the [runtimeType] and [key] properties of the two widgets are
/// [operator==], respectively, then the new widget replaces the old widget by
/// updating the underlying element (i.e., by calling [Element.update] with the
/// new widget). Otherwise, the old element is removed from the tree, the new
/// widget is inflated into an element, and the new element is inserted into the
/// tree.
///
/// In addition, using a [GlobalKey] as the widget's [key] allows the element
/// to be moved around the tree (changing parent) without losing state. When a
/// new widget is found (its key and type do not match a previous widget in
/// the same location), but there was a widget with that same global key
/// elsewhere in the tree in the previous frame, then that widget's element is
/// moved to the new location.
///
/// Generally, a widget that is the only child of another widget does not need
/// an explicit key.
///
/// See also:
///
/// * The discussions at [Key] and [GlobalKey].
机翻:
控制widget在树中替换另一个widget。
如果widget的`runtimeType`和`key`分别相等,那么新的widget会通过更新底层的element来替换旧的widget(即调用`Element.update`方法,将新的widget替换进去);否则,会从树中移除旧的element,然后将「用新widget更新过自己的element」放置在该位置。
特别的,使用GlobalKey的widget,可以在不丢失state的情况下,在树中移动element。当一个新widget(即runtimeType和key两个不能同时相等时)出现,并且前一帧的树中其他位置存在一个同样的key时,这个widget对应的element就会被移动到新的位置。(就是这里!告诉我们使用key,可以让statefulWidget带着自己的state走,即带element走(因为state被element持有!))
整个Widget类中,只有canUpdate方法使用了key,我们再看下这个方法:
/// 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;
}
机翻:
新widget是否可以用于更新那个「持有旧widget的element」。当且仅当,两个widget拥有相同的runtimeType和key时,一个element可以使用widget作为它的配置,也可以更换另一份widget作为配置。
最后来看一份Element的update方法,里面使用到了Widget.canUpdate(widget, newWidget),当且仅当两个widget的runtimeType和key相等时,element才会更新自己的widget。
/// Change the widget used to configure this element.
///
/// The framework calls this function when the parent wishes to use a
/// different widget to configure this element. The new widget is guaranteed
/// to have the same [runtimeType] as the old widget.
///
/// This function is called only during the "active" lifecycle state.
@mustCallSuper
void update(covariant Widget newWidget) {
// This code is hot when hot reloading, so we try to
// only call _AssertionError._evaluateAssertion once.
assert(_lifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& Widget.canUpdate(widget, newWidget));
// This Element was told to update and we can now release all the global key
// reservations of forgotten children. We cannot do this earlier because the
// forgotten children still represent global key duplications if the element
// never updates (the forgotten children are not removed from the tree
// until the call to update happens)
assert(() {
_debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
_debugForgottenChildrenWithGlobalKey.clear();
return true;
}());
_widget = newWidget;
}
小结一下:
只有使用key的时候,才能把widget和element链接起来,才能在widget移动的时候,将element关联地移动
看完注释之后,结合四个场景深入了解key的影响:
-
ColorBox为StatelessWidget,且颜色放在build里面
-
ColorBox为StatelessWidget,颜色由widget管理
-
ColorBox为StatefulWidget,count由state管理,不使用key
-
ColorBox为StatefulWidget,count由state管理,使用key
这四种场景的预期都是点击右下角的加号,两个Box交换位置。
Demo
ColorBox为StatelessWidget,且颜色放在build里面
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ColorStatelessBox extends StatelessWidget {
ColorStatelessBox({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
width: 100,
height: 100,
);
}
}
class ColorStatefulBox extends StatefulWidget {
const ColorStatefulBox({Key key, this.color}) : super(key: key);
final Color color;
@override
State<ColorStatefulBox> createState() => _ColorStatefulBoxState();
}
class _ColorStatefulBoxState extends State<ColorStatefulBox> {
int count = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
count++;
setState(() {});
},
child: Container(
color: widget.color,
width: 100,
height: 100,
child: Center(
child: Text(
count.toString(),
style: TextStyle(fontSize: 30),
),
),
),
);
}
}
class TestPage extends StatefulWidget {
@override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
List<Widget> widgetList = [ColorStatelessBox(), ColorStatelessBox()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("key test"),
),
body: Container(
padding: EdgeInsets.all(20),
child: Column(
children: [...widgetList],
),
),
floatingActionButton: GestureDetector(
onTap: () {
widgetList = widgetList.reversed.toList();
setState(() {});
},
child: Icon(
Icons.add,
),
));
}
}
现象:结果是每点击一次右下角的加号,两个ColorStatelessBox的颜色都会随机一次,体现不出交换两个的位置的结果。
分析:因为我们在setState的时候,Element.performRebuild调用了build函数,随机了color,所以每点击一次,颜色都会随机一次。
ColorBox为StatelessWidget,颜色由widget管理
class ColorStatelessBox extends StatelessWidget {
ColorStatelessBox({
Key key,
}) : super(key: key);
final Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
color: color,
width: 100,
height: 100,
);
}
}
改动:将color交给widget管理,不必每次build都随机生成。
现象:点击加号,可以看到确实交换了两个box的位置。
分析:因为在setState的时候,调用Element.update时,接着判断Widget.canUpdate(widget, newWidget),这里widget和newWidget都是ColorStatelessBox类型,而且都没有key参数,所以Element使用了newWidget更新自己,而这个newWidget,就是被交换顺序之后的widget,所以能看到前后两个box确实换了位置。
图片来源于参考链接中的视频,强烈推荐观看文末的参考视频。(这里widget tree中红蓝结点分别代表储存在widget中的color颜色)
ColorBox为StatefulWidget,count由state管理,不使用key
改动:ColorBox改为StatefulWidget,新增count,储存在state中
现象:两个box初始化时,分别传入yellow、blue作为颜色,点击box改变count,最后点击加号,只有颜色改变了。
分析:首先颜色的改变,同「ColorBox为StatelessWidget,颜色由widget管理」的分析。「color数据是跟着widget的」,但「count数据是跟着state的」,而「element是由widget生成,并且持有state的」。所以颜色换位置了,但是count,即state并没有换位置。我们回到上面Widget源码的第二段,因为两个box都没传key,所以仅匹配runtimeType就已经相等了,所以没有更新element tree。
ColorBox为StatefulWidget,count由state管理,使用key
List<Widget> widgetList = [
ColorStatefulBox(
color: Colors.yellow,
key: GlobalKey(),
),
ColorStatefulBox(
color: Colors.blue,
key: GlobalKey(),
)
];
改动:增加key字段
现象:两个box位置,state都一起换位了
分析:widget更换位置跟前面的分析一样,但这次element的位置也变了。原因还是Widget源码的第二段。增加了key字段之后,原本element就匹配不上换位之后的widget了,就会移除当前位置的element。再根据Widget源码的第三段,找回那个原本key对应的element,将它放到这个位置,那么,就能实现带着state移动的效果。
(这里她把颜色存在state里面,对应本例中的count)
什么场景需要使用key?
对相同类型、带状态的组件集合,进行add、remove、sort的时候。
其实经过上面四个例子的分析,可以发现,出问题的状况为:多个同类型的StatefulWidget、state保存了数据、组件位置会变化。
常见的场景如:支付宝主页的编辑入口、待办功能checkList。
留个问题,关于widget源码注释中提到的element的移除操作的方法源码我没找到,大家知道的话,可以跟我同步一下。
有问题的话,感谢指出。