顶层容器栈的实现
Boost视图栈
当发生页面切换时,Boost内部通过其自定义的顶层入口,将所有子页面都进行了一次包裹。
外层包裹的节点分别是:
BoostContainerManager => Overlay => BoostContainer
当发出页面入栈出栈时,Overlay节点所代表的的数组元素会增加。新增加的页面会插入到Overlay数组的第一个,形式上就是Stack的效果:先进后出。
顶层容器 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。
当这些完成后打印一行日志,标志着一次页面切换动作完成:
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并且添加下一帧刷新函数
最后一个动作的作用有两个:
- 再次通过channel将页面展示实践回传给native,事件名为onShownContainerChanged;
- 第二个刷新焦点,原因是这里每个OverlayEntry其实都有一个原生的Navigator;
现在我们又新增了一个疑问:
问题3:屏蔽了setState的super之后,没有副作用吗?Element如何进行待刷新标记?