在Flutter世界中,Key分为两种类型,一种是GlobalKey,一种LocalKey,LocalKey具体到实现的类型又有ObjectKey, UniqueKey,ValueKey等等… 本文我们将讨论的是GlobalKey。
GlobalKey有一个很实用的功能,可以让我们访问到其挂载的widget,context,state数据,上个栗子:
代码语言:javascript复制/// 定义色块StatefulWidget
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key? key}) : super(key: key); // NEW CONSTRUCTOR
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<StatefulColorfulTile> {
late Color myColor;
@override
void initState() {
super.initState();
Random random = Random();
myColor = Color.fromARGB(255, random.nextInt(255), random.nextInt(255), random.nextInt(255));
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
),
);
}
}
代码语言:javascript复制class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<ColorfulTileState> key = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: StatefulColorfulTile(key: key),
);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
print("color ${key.currentState?.myColor}");
});
}
}
在运行上面程序之后可以看到打印了色块widget随机生成的颜色,即通过key.currentState可以获取到state数据,同样的可以通过currentContext,currentWidget方法获取到context,widget数据。
实现原理:
我们先来看GlobalKey的定义,可以看到GlobalKey实际上就是对其对应Element的操作,我们知道Element是管理Widget,RenderObject的对象。在_currentElement get方法中通过WidgetsBinding.instance!.buildOwner!._globalKeyRegistry[this]获取了Element实例,既而通过_currentElement获取currentWidget,currentState,currentContext(这里直接返回_currentElement,因为BuildContext是一个抽象类,Element implements BuildContext)。看到这我们应该能猜到buildOwner的_globalKeyRegistry是一个Map类型,key值是GlobalKey,value则是对应的Element。那它是什么时候被注册进去的呢?我们接着看。
代码语言:javascript复制abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);
const GlobalKey.constructor() : super.empty();
Element? get _currentElement => WidgetsBinding.instance!.buildOwner!._globalKeyRegistry[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;
}
}
对Flutter三棵树挂载流程熟悉的同学应该知道,element通过inflateWidget->mount-> rebuild(ComponentElement) -> performRebuild (ComponentElement)-> updateChild->inflateWidget递归创建element树结构,在inflateWidget方法中会新建element‘’final Element newChild = newWidget.createElement();‘‘ ,接着调用newChild.mount(this, newSlot);在Element的mount方法中,我们看到了对GlobalKey的使用,在判断widget key类型是GlobalKey之后将其注册至owner的_globalKeyRegistry中。owner可能有些同学会比较陌生,owner实际是BuildOwner的实例,在WidgetBinding中持有生成,管理dirty和inactive的element。我们这不做深入讨论,只需要知道在element树构建中是会将owner传递,即共享一个owner对象。同样在unmount时会调用owner的_unregisterGlobalKey移除Globalkey。
到这应该大家就明白了,在widget创建的时候,会将GlobalKey和element的对应关系注册到owner的_globalKeyRegistry中。当我们需要访问GlobalKey时,则调用WidgetsBinding.instance!.buildOwner!._globalKeyRegistry[this]获取element,进而获取生成其他widget,state,context数据。
代码语言:javascript复制 void mount(Element? parent, Object? newSlot) {
...
if (parent != null) {
...
// 获取父element的owner
_owner = parent.owner;
}
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
_updateInheritance();
}
代码语言:javascript复制void unmount() {
...
final Key? key = _widget!.key;
if (key is GlobalKey) {
owner!._unregisterGlobalKey(key, this);
}
...
}
接下来介绍GlobaKey的另一个功能数据状态保存,同样上个栗子:
代码语言:javascript复制class _MyHomePageState extends State<MyHomePage> {
bool addParent = false;
final GlobalKey<ColorfulTileState> key = GlobalKey();
@override
Widget build(BuildContext context) {
Widget colorTile = StatefulColorfulTile(key: key,);
if (addParent) {
colorTile = Row(
children: [
colorTile
],
);
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: colorTile,
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
addParent = !addParent;
});
},
child: Icon(Icons.add),
),
);
}
}
对上面state做了下改造,增加了addParent状态,增加了floatingActionButton用于切换addParent状态,addParent会改变colorTile在Widget中的层级,可以看到在我们点击floatingActionButton切换状态的时候,colorTile的颜色并没有改变,而当我们把key去掉或者换成LocalKey类型的key,会发现每一次切换addParent状态colorTile颜色会变化。即可以在app的任何地方更换父widget而不会丢失状态。
实现原理:
在开始探索原理之前,我们需要明白setState的流程,我们简单过一下,在setState之后,会将当前widget(MyHomePage)markNeedsBuild,markNeedsBuild会将当前element标记为dirty,同时调用owner的 scheduleBuildFor(this)将当前element添加到owner的_dirtyElements里面。接着调用onBuildScheduled通知drawFrame,在上面我们说到WidgetsBinding会持有owner对象,实际上通知owner去处理dirty element也是在WidgetsBinding触发,WidgetsBinding中继承了drawFrame方法,drawFrame会调用owner.buildScope处理dirty element的rebuild,最后调用buildOwner!.finalizeTree来释放_inactiveElements。在element的rebuild中会调用performRebuild(不同element会有不同的performRebuild实现,例如ComponentElement会执行build方法,然后调用updateChild递归child更新,RenderObjectElement则是更新RenderObject,如果是带child或children的RenderObjectElement最终也会调用到updateChild),到这我们知道实际上widget的更新重建是最终都是在updateChild中递归执行。
updateChild(Element? child, Widget? newWidget, Object? newSlot)中会根据参数操作不同,如下所示。在本文例子中,我们更改了层级,在updateChild方法中会进入deactivateChild(child);,然后重新inflateWidget生成新的element树。
代码语言:javascript复制 /// The following table summarizes the above:
///
/// | | **newWidget == null** | **newWidget != null** |
/// | :-----------------: | :--------------------- | :---------------------- |
/// | **child == null** | Returns null. | Returns new [Element]. |
/// | **child != null** | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
///
在deactivateChild中,实际上并没有立即释放掉element,而且将element的parent关联清除,解除RenderObject的关联并将其添加到owner的_inactiveElements中。那么这个被添加到_inactiveElements的element还有没有机会被复用呢。答案是有的,在inflateWidget递归构建新的element树时,会判断widget key是否是GlobalKey,是的话尝试调用_retakeInactiveElement,_retakeInactiveElement会复用取出GlobalKey对应的element(这里需要注意_retakeInactiveElement有先后问题,如果我们复用的widget已经被deactivateChild,那么在_retakeInactiveElement中判断parent为空就不会调用释放。如果parent不为空,说明widget目前还在别的树上挂载,还没执行到deactivateChild方法,这时会先执行parent.forgetChild,parent.deactivateChild将element的挂载和关联清除),owner将element从_inactiveElements移除,然后复用element updateChild,state同样得以复用保存,所以色块颜色不会发生变化。
代码语言:javascript复制Element inflateWidget(Widget newWidget, Object? 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!;
}
}
...
}
代码语言:javascript复制Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
...
final Element? element = key._currentElement;
if (element == null)
return null;
if (!Widget.canUpdate(element.widget, newWidget))
return null;
...
final Element? parent = element._parent;
if (parent != null) {
...
parent.forgetChild(element);
parent.deactivateChild(element);
}
assert(element._parent == null);
owner!._inactiveElements.remove(element);
return element;
}
题外话:假如我们在StatefulColorfulTile的build方法中增加一个打印print(“build”);,大家可以思考下在我们切换addParent的时候会不会打印呢?答案是会的,但是build方法调用不代表我们的widget被重新绘制,在Flutter中build方法是生成widget配置信息的,是很轻量也是会被频繁调用。在本文例子中StatefulColorfulTile是StatefulWidget,对应的是StatefulElement,StatefulElement的父类ComponentElement当调用到performRebuild时,会调用build方法创建新的widget(就是在这里会执行我们的打印),updateChild时会传入我们的新widget来更新旧widget组件,widget会被更新,而复用的是比较重量的element。
总结一下,上面我们主要介绍了GlobalKey的两种应用场景及原理:
- 可以在app的任何地方更换父widget而不会丢失状态
- 它可以用来从完全不同的widget树里面访问数据
下一篇文章,我们将继续探索Key的另一个家族LocalKey的应用及实现原理。
待续…