开始
在Flutter的每个Widget中, 都会有key这个可选属性. 在刚开始学习flutter时, 基本就直接忽略不管了. 对执行结果好像也没什么影响. 现在来深究下key到底有什么作用.(研究一天时间, 发现key
没什么作用. 快要放弃, 又多写了几个简单例子, 终于发现差异~~)
官方文档介绍
key用于控制控件如何取代树中的另一个控件.
如果2个控件的runtimeType
和key
属性operator==
, 那么新的控件通过更新底层元素来替换旧的控件(通过调用Element.update
). 否则旧的控件将从树上删除, element
会生成新的控件, 然后新的element
会被插入到树中.
另外, 使用GlobalKey
做为控件的key
, 允许element
在树周围移动(改变父节点), 而不会丢失状态. 当发现一个新的控件时(它的key
和类型与同一位置上控件不匹配), 但是在前面的结构中有一个带有相同key
的小部件, 那么这个控件将会被移动到新的位置.
GlobalKey
是很昂贵的. 如果不需要使用上述特性, 可以考虑使用Key
, ValueKey
或UniqueKey
替换.
通常, 只有一个子节点的widget
不需要指定key
.
实践
为了弄清楚这些, 我们有必要了解, Flutter中控件的构建流程以及刷新流程. 简单的做法是在StatelessWidget.build
或者StatefulWidget.build
中下个断点, 调试运行, 等断点停下来. 在Debug
视窗的Frames
视图下可以看到函数的调用堆栈. 然后继续跑几步, 可以看到后面的调用流程.
调用流程
为了更直观的查看调用流程, 省去了MaterialApp
和Scaffold
的包裹.
例子1
void main() {
runApp(Sample1());
}
class Sample1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('Sample1', textDirection: TextDirection.ltr,);
}
}
从这个调用栈我们就可以知道调用流程. 关键函数我直接提炼出来.
关键代码分析
关键代码
-
Widget.canUpdate
, 用于判断Widget
是否能复用, 注意类型相同,key
为空也是可以复用的.static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
Element.updateChild
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
....
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
}());
return child;
}
deactivateChild(child);
assert(child._parent == null);
}
return inflateWidget(newWidget, newSlot);
}
第一次构建时, Element child
参数为空, 我们需要将StatefulWidget
或StatelessWidget
build出的Widget
传递给inflateWidget方法. 后面刷新界面时, Element child
参数不为空时, 我们可以判断Element原来持有的widget和build新得出的widget是否相同. 什么情况下会这样了? 当我们缓存了Widget就会这样.
例子2
void main() {
runApp(Sample2());
}
class _Sample2State extends State<Sample2> {
int count = 0;
Text cacheText;
@override
void initState() {
cacheText = Text(
'cache_text',
textDirection: TextDirection.ltr,
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
cacheText,
Text('no_cache_text', textDirection: TextDirection.ltr),
],
);
}
}
在if (child.widget == newWidget)
这里下个断点, 当点击按钮事刷新时, 我们就可以发现cache_text
会进来, 而no_cache_text
不会进来. no_cache_text
会进入到if (Widget.canUpdate(child.widget, newWidget))
里, 因为parent(Column)没变, 元素个数没变, 继续执行update(widget)
@mustCallSuper
void update(covariant Widget newWidget) {
assert(_debugLifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& _active
&& Widget.canUpdate(widget, newWidget));
_widget = newWidget;
}
这里其实就是修改了Element
持有的_widget
指向. 也就是文档里说的, Widget
被切换, 而Element
会被复用. 当这些都无法满足时, 就是执行inflateWidget
来创建新的Element
. 什么情况下会这样了, 当Widget.canUpdate不为真时, 就是当类型或key不同时. 下面举个例子:
例子3
class _Sample3State extends State<Sample3> {
int count = 0;
GlobalKey keyOne = GlobalKey();
GlobalKey keyTwo = GlobalKey();
@override
void initState() {}
@override
Widget build(BuildContext context) {
List<Widget> list = [
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
Text(
'key${count%2}',
textDirection: TextDirection.ltr,
key: count % 2 == 0 ? keyOne : keyTwo,
),
];
if (count % 2 == 0) {
list.add(RaisedButton(
onPressed: () {},
child: Text('button text', textDirection: TextDirection.ltr)));
} else {
list.add(Text('just text', textDirection: TextDirection.ltr));
}
return Column(
children: list,
);
}
}
在Element.inflateWidget
下个断点, 每次点击按钮, 都会调用inflateWidget
生Column
的后面2个Widget
. 如果去掉第二个控件的key
属性, 则不会每次都执行Element.inflateWidget
. 也就是说一般情况下, 我们无需指定key
属性, Element
就能复用.
文档里还指出使用了GlobalKey
, 一个Element
可以从树的一个位置复用到树的其它位置. 其相关核心代码在inflateWidget
和_retakeInactiveElement
方法里.
Element.inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
assert(newWidget != null);
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
assert(newChild._parent == null);
assert(() { _debugCheckForCycles(newChild); return true; }());
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
assert(() { _debugCheckForCycles(newChild); return true; }());
newChild.mount(this, newSlot);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
return newChild;
}
Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
final Element element = key._currentElement;
if (element == null)
return null;
if (!Widget.canUpdate(element.widget, newWidget))
return null;
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle)
debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this.');
return true;
}());
final Element parent = element._parent;
if (parent != null) {
assert(() {
if (parent == this) {
throw new FlutterError(
...
);
}
parent.owner._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent,
key,
);
return true;
}());
parent.forgetChild(element);
parent.deactivateChild(element);
}
assert(element._parent == null);
owner._inactiveElements.remove(element);
return element;
}
代码大概意思是, 首先如果widget
的key
值不为空并且为GlobalKey
类型时, 会判断key._currentElement
值所指向的widget
, 和当前widget
的类型key
都相同. 那么就从旧的父节点上移除. 作为当前的节点的子widget
之一. 否则将进行真实的创建新的Element
. 下面举个例子
例子4
void main() {
runApp(Sample4());
}
class Sample4 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _Sample4State();
}
}
class _Sample4State extends State<Sample4> {
int count = 0;
GlobalKey key = GlobalKey();
@override
Widget build(BuildContext context) {
var child;
if (count % 2 == 0) {
child = Padding(
padding: EdgeInsets.all(10.0),
child: Text(
'text 2',
textDirection: TextDirection.ltr,
key: key,
),
);
} else {
child = Container(
child: Text(
'text 2',
textDirection: TextDirection.ltr,
key: key,
),
);
}
return Column(
children: <Widget>[
RaisedButton(
child: Text(
'$count',
textDirection: TextDirection.ltr,
),
onPressed: () {
print(this.widget);
setState(() {
count += 1;
});
},
),
child
],
);
}
}
在Element._retakeInactiveElement
打个断点, 会发现Padding
里的text 1
与Container
里的text 2
复用了.
GlobalKey
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
Element get _currentElement => _registry[this];
BuildContext get currentContext => _currentElement;
Widget get currentWidget => _currentElement?.widget;
T get currentState {
final Element element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T)
return state;
}
return null;
}
...
void GlobalKey._register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
_debugIllFatedElements.add(_registry[this]);
}
return true;
}());
_registry[this] = element;
}
....
}
void Element.mount(Element parent, dynamic newSlot) {
...
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
...
}
当过阅读代码我们可以知道GlobalKey
在Element
被创建时就写入到一个静态Map
里, 并且关联了当前的Element
对象. 所以通过GlobalKey
可以查询当前控件相关的信息. 下面举个例子
例子5
void main() {
runApp(Sample5());
}
class Sample5 extends StatefulWidget {
@override
State createState() {
return _Sample5State();
}
}
class _Sample5State extends State<Sample5> {
final key = GlobalKey<Sample5WidgetState>();
@override
void initState() {
//calling the getHeight Function after the Layout is Rendered
WidgetsBinding.instance.addPostFrameCallback((_) => getHeight());
super.initState();
}
void getHeight() {
final Sample5WidgetState state = key.currentState;
final BuildContext context = key.currentContext;
final RenderBox box = state.context.findRenderObject();
print(state.number);
print(box.size.height);
print(context.size.height);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Sample5Widget(
key: key,
),
),
);
}
}
class Sample5Widget extends StatefulWidget {
Sample5Widget({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => Sample5WidgetState();
}
class Sample5WidgetState extends State<Sample5Widget> {
int number = 12;
@override
Widget build(BuildContext context) {
return new Container(
child: new Text(
'text',
style: const TextStyle(fontSize: 32.0, fontWeight: FontWeight.bold),
),
);
}
}
通过将GlobalKey
传递给下层, 我们在上层通过GlobalKey
能够获取到Sample5WidgetState
对象.
除了上面所述, 那么什么时候我们需要使用key
? 官方有个例子: Implement Swipe to Dismiss, 为每个item指定了key
属性'Key'(不是GlobaKey
). 也就是对于列表, 为了区分不同的子项, 也可能用到key
. 一般key
值与当前item的数据关联. 刷新时, 同一个数据指向的item复用, 不同的则无法复用.
总结
- 对于列表可以使用
key
唯一关联数据. -
GlobaKey
可以让不同的页面复用视图. 参见例子4 -
GlobaKey
可以查询节点相关信息. 参见例子5
执行流程先复用widget
, 不行就创建widget
复用Element
, 再不行就都重新创建.