Flutter进阶-Key之GlobalKey

2022-05-11 18:42:45 浏览数 (1)

在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数据,同样的可以通过currentContextcurrentWidget方法获取到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创建的时候,会将GlobalKeyelement的对应关系注册到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的应用及实现原理。

待续…

0 人点赞