顶层容器栈的实现

Boost视图栈

当发生页面切换时,Boost内部通过其自定义的顶层入口,将所有子页面都进行了一次包裹。

boost-manager

外层包裹的节点分别是:

BoostContainerManager => Overlay => BoostContainer

boost_widget_stack

当发出页面入栈出栈时,Overlay节点所代表的的数组元素会增加。新增加的页面会插入到Overlay数组的第一个,形式上就是Stack的效果:先进后出。

boost-overlay

顶层容器 BoostContainerManager

这个类的主要是作为入口,负责管理页面路由信息。

可以看到他定义了针对页面的四种操作:

  • push 入栈
  • onstage 页面后台
  • pop 出栈
  • remove 移除(这个和pop的区别后面分析)

视图栈的实现

整个Manager的页面构建非常简单,就是利用了Overlay组件,实现了页面的层叠。这样每一层自身还可以使用原生的路由栈。

@override
Widget build(BuildContext context) {
  return Overlay(
    key: _overlayKey,
    initialEntries: const <OverlayEntry>[],
  );
}

可以预见的时,我们调用的open操作最终也会通过channel再次从native侧回到dart侧,并修改数组元素。

那么这个channel对应的方法是什么?

还记得前面我们打印的页面栈切换是的日志么,答案就在其中。我们搜索一下willShowPageContainer这个定义好的方法名,可以找到蛛丝马迹:

/// container_cordinator.dart
Future<dynamic> _onMethodCall(MethodCall call) {
    Logger.log("onMetohdCall ${call.method}");
  case "willShowPageContainer":
      {
        String pageName = call.arguments["pageName"];
        Map params = call.arguments["params"];
        String uniqueId = call.arguments["uniqueId"];
        _nativeContainerWillShow(pageName, params, uniqueId);
      }
      break;
}
bool _nativeContainerWillShow(String name, Map params, String pageId) {
  if (FlutterBoost.containerManager?.containsContainer(pageId) != true) {
    FlutterBoost.containerManager
      ?.pushContainer(_createContainerSettings(name, params, pageId));
  }

  //TODO, 需要验证android代码是否也可以移到这里
  if (Platform.isIOS) {
    try {
      final SemanticsOwner owner =
        WidgetsBinding.instance.pipelineOwner?.semanticsOwner;
      final SemanticsNode root = owner?.rootSemanticsNode;
      root?.detach();
      root?.attach(owner);
    } catch (e) {
      assert(false, e.toString());
    }
  }
  return true;
}

对应的dart侧实现位于函数:_nativeContainerWillShow

我们可以看到,会基于页面的pageid进行前置校验,通过后,则会调用Manager对应State的pushContainer函数,进行页面切换。

页面入栈实现

现在我们继续回到Manager上下文,分析对应的函数实现。

void pushContainer(BoostContainerSettings settings) {
  assert(settings.uniqueId != _onstage.settings.uniqueId);
  assert(_offstage.every((BoostContainer container) =>
                         container.settings.uniqueId != settings.uniqueId));

  _offstage.add(_onstage);
  _onstage = BoostContainer.obtain(widget.initNavigator, settings);

  setState(() {});

  for (BoostContainerObserver observer in FlutterBoost
       .singleton.observersHolder
       .observersOf<BoostContainerObserver>()) {
    observer(ContainerOperation.Push, _onstage.settings);
  }
  Logger.log('ContainerObserver#2 didPush');
}

这个函数做了三件事情:

  • 页面合法性校验,基于页面的唯一标识uniqueId,作用是防止重复入栈。可以结合containsContainer方法。
  • 将当前的页面加入offstage列表中,重新生成新的目标页面。
  • 同镇State数据变更,同时触发观察者回调函数的执行。

以Example中的页面为例,当我们点击打开第二个Flutter页面后,Widget节点中插入了一个SecondRouteWidget。

boost_widget_stack2

当这些完成后打印一行日志,标志着一次页面切换动作完成:

ContainerObserver#2 didPush

这里有两个细节指的注意,分别是两个断言处理:

assert(settings.uniqueId != _onstage.settings.uniqueId);
assert(_offstage.every((BoostContainer container) =>
    container.settings.uniqueId != settings.uniqueId));

在执行入栈pushContainer函数前有一个判断:FlutterBoost.containerManager?.containsContainer(pageId) != true

只有当整个条件满足时才会触发入栈操作,整个条件会屏蔽两种尝试入栈的情况:

  • 当前显示页面已经是需要入栈的页面,规避一个页面id多次调用入栈
  • 目标页面处于offstage队列中,如果要显示的页面已经在后台队列中,则忽略当次入栈

这两个情况,第一个好理解,但是第二个初看起来是有瑕疵的:

问题1:加上目标页面已经存在于后台队列,那么如果要显示,是不是应该把他前面的页面全部出栈,然后将他显示出来?

这个疑问我们先放一放,后面再分析。

页面栈刷新逻辑

到这里,不知道你有没有疑问,反正我是有的。

问题2:入栈函数调用setState({})触发了Widget刷新,但是build函数内的依赖关系并没有被改变,如何刷新?

@override
Widget build(BuildContext context) {
    return Overlay(
        key: _overlayKey,
        initialEntries: const <OverlayEntry>[],
    );
}

正常来说修改稿state一定是在{}中改变的状态属性,并且该属性会被build直接、间接引用,才能达到刷新目的。

检索一下Manager的上下文,答案很快就揭晓了:setState函数被重载了。

@override
void setState(VoidCallback fn) {
  if (SchedulerBinding.instance.schedulerPhase ==
      SchedulerPhase.persistentCallbacks) {
    SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
      _refreshOverlayEntries();
    });
  } else {
    _refreshOverlayEntries();
  }

  fn();
  //return super.setState(fn);
}

如果当前Flutter正在进程绘制调度,那么将刷新动作延迟加入下一次绘制的回调函数中。根据API注释,可以知道当persisntCallback代表build/layout/paint之一正在被执行

/// The persistent callbacks (scheduled by
/// [WidgetsBinding.addPersistentFrameCallback]) are currently executing.
///
/// Typically, this is the build/layout/paint pipeline. See
/// [WidgetsBinding.drawFrame] and [SchedulerBinding.handleDrawFrame].
persistentCallbacks,

最后执行传入setState的{}函数。所以说对Overlay的操作就是在_refreshOverlayEntries中进行的。这里还有一个注意点,重载的setState屏蔽了super函数。

在修改Overlay的过程中,主要分几步:

  • 移除现有的overlay节点
  • 构造新的BoostConatiner数组,并且map转换为OverlayEntry
  • 最后更新OverlayState并且添加下一帧刷新函数

最后一个动作的作用有两个:

  1. 再次通过channel将页面展示实践回传给native,事件名为onShownContainerChanged;
  2. 第二个刷新焦点,原因是这里每个OverlayEntry其实都有一个原生的Navigator;

现在我们又新增了一个疑问:

问题3:屏蔽了setState的super之后,没有副作用吗?Element如何进行待刷新标记?

powered by Gitbook最近更新 2020-03-23

results matching ""

    No results matching ""