说说Flutter中最熟悉的陌生人 —— Key
Posted 唯鹿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了说说Flutter中最熟悉的陌生人 —— Key相关的知识,希望对你有一定的参考价值。
Key
在Flutter的源码中可以说是无处不在,但是我们日常中确不怎么使用它。有点像是“最熟悉的陌生人”,那么今天就来说说这个“陌生人”,揭开它神秘的面纱。
概念
Key
是Widget
、Element
和SemanticsNode
的标识符。 只有当新的Widget
的Key
与当前Element
中Widget
的Key
相同时,它才会被用来更新现有的Element
。Key
在具有相同父级的Element
之间必须是唯一的。
以上定义是源码中关于Key
的解释。通俗的说就是Widget
的标识,帮助实现Element
的复用。关于它的说明源码中也提供了YouTube的视频链接:When to Use Keys。如果你无法访问,可以看Google 官方在优酷上传的:
When to Use Keys
例子
视频中的例子很简单且具有代表性,所以本文将采用它来介绍今天的内容。
首先上代码:
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Home Page'),
);
class MyHomePage extends StatefulWidget
MyHomePage(Key key, this.title) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
class _MyHomePageState extends State<MyHomePage>
List<Widget> widgets;
@override
void initState()
super.initState();
widgets = [
StatelessColorfulTile(),
StatelessColorfulTile()
];
@override
Widget build(BuildContext context)
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
children: widgets,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: _swapTile,
),
);
_swapTile()
setState(()
widgets.insert(1, widgets.removeAt(0));
);
class StatelessColorfulTile extends StatelessWidget
final Color _color = Utils.randomColor();
@override
Widget build(BuildContext context)
return Container(
height: 150,
width: 150,
color: _color,
);
class Utils
static Color randomColor()
var red = Random.secure().nextInt(255);
var greed = Random.secure().nextInt(255);
var blue = Random.secure().nextInt(255);
return Color.fromARGB(255, red, greed, blue);
代码可以直接复制到DartPad中运行查看效果。 或者点击这里直接运行。
效果很简单,就是两个彩色方块,点击右下角的按钮后交换两个方块的位置。这里我就不放具体的效果图了。实际效果也和我们预期的一样,两个方块成功交换位置。
发现问题
上面的方块是StatelessWidget
,那我们把它换成StatefulWidget
呢?。
class StatefulColorfulTile extends StatefulWidget
StatefulColorfulTile(Key key) : super(key: key);
@override
StatefulColorfulTileState createState() => StatefulColorfulTileState();
class StatefulColorfulTileState extends State<StatefulColorfulTile>
final Color _color = Utils.randomColor();
@override
Widget build(BuildContext context)
return Container(
height: 150,
width: 150,
color: _color,
);
再次执行代码,发现方块没有“交换”。这是为什么?
分析问题
首先要知道Flutter中有三棵树,分别是Widget Tree、Element Tree 和 RenderObject Tree。
- Widget:
Element
的配置信息。与Element
的关系可以是一对多,一份配置可以创造多个Element
实例。 - Element:
Widget
的实例化,内部持有Widget
和RenderObject
。 - RenderObject:负责渲染绘制。
简单的比拟一下,Widget
有点像是产品经理,规划产品整理需求。Element
则是UI小姐姐,根据原型整理出最终设计图。RenderObject
就是我们程序员,负责具体的落地实现。
代码中可以确定一点,两个方块的Widget肯定是交换了。既然Widget
没有问题,那就看看Element
。
但是为什么StatelessWidget
可以成功,换成StatefulWidget
就失效了?
点击按钮调用setState
方法,依次执行:
我们重点看一下Element
的updateChild
方法:
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot)
// 如果'newWidget'为null,而'child'不为null,那么我们删除'child',返回null。
if (newWidget == null)
if (child != null)
deactivateChild(child);
return null;
if (child != null)
// 两个widget相同,位置不同更新位置,返回child。这里比较的是hashCode
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);
return child;
deactivateChild(child);
// 如果无法更新复用,那么创建一个新的Element并返回。
return inflateWidget(newWidget, newSlot);
Widget
的canUpdate
方法:
static bool canUpdate(Widget oldWidget, Widget newWidget)
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
这里出现了我们今天的主角Key
,不过我们先放在一边。canUpdate
方法的作用是判断newWidget是否可以替代oldWidget作为Element
的配置。 一开始也提到了,Element
会持有Widget。
该方法判断的依据就是runtimeType
和key
是否相等。在我们上面的例子中,不管是StatelessWidget
还是StatefulWidget
的方块,显然canUpdate
都会返回true。因此执行child.update(newWidget)
方法,就是将持有的Widget更新了。
不知道这里大家有没有注意到,这里并没有更新state
。我们看一下StatefulWidget
源码:
abstract class StatefulWidget extends Widget
const StatefulWidget( Key key ) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
StatefulWidget
中创建的是StatefulElement
,它是Element
的子类。
class StatefulElement extends ComponentElement
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget)
_state._element = this;
_state._widget = widget;
@override
Widget build() => state.build(this);
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
...
通过调用StatefulWidget
的createElement
方法,最终执行createState
创建出state并持有。也就是说StatefulElement
才持有state。
所以我们上面两个StatefulWidget
的方块的交换,实际只是交换了“身体”,而“灵魂”没有交换。所以不管你怎么点击按钮都是没有变化的。
解决问题
找到了原因,那么怎么解决它?那就是设置一个不同的Key
:
@override
void initState()
super.initState();
widgets = [
StatefulColorfulTile(key: const Key("1")),
StatefulColorfulTile(key: const Key("2"))
];
但是这里要注意的是,这里不是说添加key以后,在canUpdate
方法返回false,最后执行inflateWidget(newWidget, newSlot)
方法创建新的Element
。(很多相关文章对于此处的说明都有误区。。。好吧我承认我一开始也被误导了。。。)
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot)
final Key key = newWidget.key;
if (key is GlobalKey)
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null)
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
// 这里就调用到了createElement,重新创建了Element
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
如果如此,那么执行createElement
方法势必会重新创建state,那么方块的颜色也就随机变了。当然此种情况并不是不存在,比如我们给现有的方块外包一层Padding
(SingleChildRenderObjectElement
):
@override
void initState()
super.initState();
widgets = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: Key("1"),)
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: Key("2"),)
),
];
这种情况下,交换后比较外层Padding
不变,接着比较内层StatefulColorfulTile
,因为key不相同导致颜色随机改变。因为两个方块位于不同子树,两者在逐层对比中用到的就是canUpdate
方法返回false来更改。
而本例是方块的外层是Row
(MultiChildRenderObjectElement
),是对比两个List,存在不同。关键在于update
时调用的RenderObjectElement.updateChildren
方法。
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, Set<Element> forgottenChildren )
...
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>(newWidgets.length);
Element previousChild;
// 从前往后依次对比,相同的更新Element,记录位置,直到不相等时跳出循环。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom))
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
// 注意这里的canUpdate,本例中在没有添加key时返回true。
// 因此直接执行updateChild,本循环结束返回newChildren。后面因条件不满足都在不执行。
// 一旦添加key,这里返回false,不同之处就此开始。
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
// 从后往前依次对比,记录位置,直到不相等时跳出循环。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom))
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
// 至此,就可以得到新旧List中不同Weiget的范围。
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element> oldKeyedChildren;
// 如果存在中间范围,扫描旧children,获取所有的key与Element保存至oldKeyedChildren。
if (haveOldChildren)
oldKeyedChildren = <Key, Element>;
while (oldChildrenTop <= oldChildrenBottom)
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null)
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
// 没有key就移除对应的Element
deactivateChild(oldChild);
oldChildrenTop += 1;
// 更新中间不同的部分
while (newChildrenTop <= newChildrenBottom)
Element oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren)
final Key key = newWidget.key;
if (key != null)
// key不为null,通过key获取对应的旧Element
oldChild = oldKeyedChildren[key];
if (oldChild != null)
if (Widget.canUpdate(oldChild.widget, newWidget))
oldKeyedChildren.remove(key);
else
oldChild = null;
// 本例中这里的oldChild.widget与newWidget hashCode相同,在updateChild中成功被复用。
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
// 重置
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
// 将后面相同的Element更新后添加到newChildren,至此形成新的完整的children。
while ((oldChildrenTop <= oldChildrenBottom) &&
(newChildrenTop <= newChildrenBottom))
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop]说说Flutter中的Semantics