引言
本来想很全面的分析一次Navigator1.0 原理,但是发现很难从全貌的角度来讲清楚,最后决定选择从Push流程切入,这样读者既好理解又比较容易通过专一流程尝试了解全貌。
在我上一篇Nav的封装工具类分享中有提过Nav1.0和2.0的区别
- Navigator 1.0:侧重于移动端的路由系统,采用命令式编程风格,通过push和pop方法管理路由栈。
- Navigator 2.0:在Flutter 1.22版本后新增,侧重于复杂的桌面端/网页端,采用声明式编程风格,使用Router和RouterInformationParser等类来描述和管理路由树。
路由的简单用法我就不详细去重复说明了,一开始我们先了解一下路由的注册方式,再去深入Push整个流程~
1. 路由注册的方式
1.1 静态/命名路由
首先说下命名路由,就是 需要提前注册的路由,先给每个路由定义一个「字符串名称」,然后通过这个名称来导航到对应的路由,在项目中我们偏于这种方式来注册与跳转路由。
- 注册流程
MaterialApp(
title: "Demo App",
routes: {"/main":(context)=>DemoPage()}
)
- 跳转方式
Navigator.of(context).pushNamed("/main");
1.2 动态路由
不需要提前注册,可以直接在代码中创建和导航的路由,适用于简单使用或单次导航的场景。
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainScreen()),
);
2. Push流程探索
这里我从pushNamed方法开始讲述,最后会走到跟push一样的流程~
2.1 pushNamed
先把方法放上来,我们看看具体实现。
Future<T?> pushNamed<T extends Object?>(
String routeName, {
Object? arguments,
}) {
return push<T?>(_routeNamed<T>(routeName, arguments: arguments)!);
}
-
参数分析
- routerName:路由的Key,字符串类型,我们可以直接推测出负责从路由Map中获取其builder用的。
- arguments:路由参数,我们可以通过ModalRoute.of(context)?.settings.arguments;来获取传递的参数
-
返回值:Future<T?> 页面在pop的时候,会利用Future来通知并且返回页面结束的参数
jumpPage() async { //页面结束时候会释放await,获取返回值或者通过释放await来获知页面弹出 dynamic result = await Navigator.push( context, MaterialPageRoute(builder: (context) => MainScreen()), ); } -
方法分析 调用了push方法,但是首先调用了_routerNamed方法,我们先看看routerName这个方法做了什么做了什么
Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
//当onGenerateRoute为空时,是否直接返回空
if (allowNull && widget.onGenerateRoute == null) {
return null;
}
//组装RouteSettings,把参数和路径装载进去
final RouteSettings settings = RouteSettings(
name: name,
arguments: arguments,
);
//调用onGenerateRoute方法,返回一个Route对象
Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
//如果找不到路由,则调用onUnknownRoute来返回一个容错路由
if (route == null && !allowNull) {
route = widget.onUnknownRoute!(settings) as Route<T?>?;
}
return route;
}
我们阅读注释得知方法两个重点
- RouteSetting对象存着arguments参数
- 通过onGenerateRoute,生成Route对象,那么我们接着看onGenerateRoute方法的具体实现
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
final String? name = settings.name;
//1.先判断是否是首页,如果是首页则不浪费性能,直接返回首页的WidgetBuilder
//2.如果不是首页,则从我们的RouteMap中寻找到WidgetBuilder
final WidgetBuilder? pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
? (BuildContext context) => widget.home!
: widget.routes![name];
//找到了页面对应的WidgetBuilder则往下走
if (pageContentBuilder != null) {
//3.把RouterSetting和WidgetBuilder再封装成Route对象返回
//这里的pageRouteBuilder会默认包装成MaterialPageRoute类,影响了进场出场动画等,暂时无需过度关注
final Route<dynamic> route = widget.pageRouteBuilder!<dynamic>(
settings,
pageContentBuilder,
);
return route;
}
if (widget.onGenerateRoute != null) {
return widget.onGenerateRoute!(settings);
}
return null;
}
至此routerName方法流程弹栈,我们得知了routerName就是从Map中寻找对应的WidgetBuilder,并且把路径、参数与WidgetBuilder封装到Route对象中,接着我们回去push方法看看。
2.2 push方法流程
class Navigator{
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
//1.调用of方法寻找NavigatorState,这里不展开说,然后调用NavigatorState的push方法
return Navigator.of(context).push(route);
}
}
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
//2.最后会调用到该方法
Future<T?> push<T extends Object?>(Route<T> route) {
//3.封装一个_RouteEntry对象,并且设置状态为push,然后调用_pushEntry方法
//状态这边我们下面展开说,push状态目前可以理解成目前状态为准备push中
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
//4.返回一个Future,这个Future在pop时会调用其complete方法,并且把有可能存在的页面结束参数通过complete方法传递
//我们知道了为什么push方法能够await和接收参数,再详细的我们后面再说。先聚焦push流程
return route.popped;
}
}
看完上面源码,我们的下一步必然是跟进==pushEntry方法==,但是我们先简单了解一下_RouteEntry的状态有哪些,了解状态是怎么流转的,才能更好的理解后续流程,我们先看看官方状态注释。
// The _RouteLifecycle state machine (only goes down):
//
// [creation of a _RouteEntry]
// |
// +
// |\
// | \
// | staging
// | /
// |/
// +-+----------+--+-------+
// / | | |
// / | | |
// / | | |
// / | | |
// / | | |
// pushReplace push*(当前) add* replace*
// \ | | |
// \ | | /
// +--pushing# adding /
// \ / /
// \ / /
// idle--+-----+
// / \
// / +------+
// / | |
// / | complete*
// | | /
// pop* remove*
// / \
// / removing#
// popping# |
// | |
// [finalizeRoute] |
// \ |
// dispose*
// |
// |
// disposed
// |
// |
// [_RouteEntry garbage collected]
// (terminal state)
//
我们可以看出idle是路由状态的分水岭,idel之前的状态,都是路由正在入栈的状态,idel后面的状态,基本是弹栈有关的状态。
那么我们根据这个状态,我们可以看到接下来我们的目的地就是了解路由如何到达idle状态,并且到达idle状态后页面是怎么显示出来的,接着我们继续跟进pushEntry方法,看看状态是怎么流转的。
2.3 状态流转流程
之前的流程只是做前期准备,各种封装,状态的初始化,接下来就是开始状态流程到页面上屏的过程了,刚刚说到我们跟进到pushEntry方法,那么我们继续看看源码。
接下来的流程会较长,我们可以直接把结论说出来,有着结论的引导我们在流程中就不会感受到漫长而迷茫。
结论:
Navigator是通过Overlay来渲染页面的,也就意味着我们的路由要显示到屏幕上的话我们必须得知Route是怎么创建OverlayEntry的,第二是Navigator的组件究竟是怎么回事,不过我们首先知道Route是怎么创建OverlayEntry的吧!
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
//...省略其他不相关代码
//当前的路由栈
List<_RouteEntry> _history = <_RouteEntry>[];
void _pushEntry(_RouteEntry entry) {
//1.把路由添加到栈中
_history.add(entry);
//2.整理路由栈,开始状态流转
_flushHistoryUpdates();
}
//该方法我删掉了与push流程无关的代码,为了专注的跟进流程。
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
//当前页面总数
int index = _history.length - 1;
//当前路由的下一个路由
_RouteEntry? next;
//当前路由(取的是栈顶路由)
_RouteEntry? entry = _history[index];
//当前路由的上一个路由
_RouteEntry? previous = index > 0 ? _history[index - 1] : null;
while (index >= 0) {
switch (entry!.currentState) {
//状态从push开始,所以我们跟进该流程分支
case _RouteLifecycle.push:
//1. 调用handlePush方法
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,
);
//2. 从这里到下面的代码我们先忽略,讲完handlePush方法再回来看
if (entry.currentState == _RouteLifecycle.idle) {
continue;
}
break;
//...忽略一些代码,不然太长干扰焦点,后面会补充的
}
}
}
}
我们继续跟进handlePush方法
void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
//获取当前状态,根据刚刚的流程,当前状态为push
final _RouteLifecycle previousState = currentState;
route._navigator = navigator;
//从命名可以直观得知正在装载路由,而该核心是为页面创建OverlayEntry。
route.install();
//...忽略代码,先专注install流程,后面会接上
}
接着跟进router.install方法看看是怎么装载的,在看源码前我们先确定当前Route的实体类为MaterialPageRoute,但是我们在MaterialPageRoute中没找到install方法的实现,那么我就一直往父类去找,我先展示一下继承路径,不然会有点晕
MaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route
在这么长的继承关系中,我们需要关注OverlayRoute和ModalRoute这两个父类。
install最后会调用到OverlayRoute中的install方法,我们跟进一下OverlayRoute这个类。
我们在结论也说到,Navigator是通过Overlay来装载页面的,所以可以确定的是OverlayRoute是负责提供OverlayEntry的一个关键类,我们来看看他的关键构造。
abstract class OverlayRoute<T> extends Route<T> {
OverlayRoute({
super.settings,
});
//创建具体的OverlayEntry迭代器
@factory
Iterable<OverlayEntry> createOverlayEntries();
//保存当前页面所拥有的OverlayEntry
@override
List<OverlayEntry> get overlayEntries => _overlayEntries;
final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
//刚刚的route.install会调用到这里。
@override
void install() {
//调用到createOverlayEntries创建路由所需的OverlayEntry,但是我们看出该类不负责具体的创建,接着我们跟进到ModalRoute的createOverlayEntries方法中。
_overlayEntries.addAll(createOverlayEntries());
super.install();
}
}
接着我们具体的跟到ModalRoute中,看看创建了一个怎么样的OverlayEntry
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
//蒙层OverlayEntry
late OverlayEntry _modalBarrier;
//页面OverlayEntry
late OverlayEntry _modalScope;
//创建OverlayEntry
@override
Iterable<OverlayEntry> createOverlayEntries() {
return <OverlayEntry>[
//创建蒙层Overlay,比如我们dialog中就是灰色底的,具体就是这个蒙层
_modalBarrier = OverlayEntry(builder: _buildModalBarrier),
//创建页面的Overlay,留意builder指向了_buildModalScope中
_modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState),
];
}
//该方法就是返回具体的页面Widget了,但是经过层层包装,路径极长,我们还记得注册静态路由的时候的WidgetBuilder吗,ModalScope最终就是调用到widgetBuilder,我看源码的时候都看晕了。
//输出完痛苦的情绪我们继续,我们省略掉ModalScope的具体过程,因为涉及的类过多路径过长,只能直接说具体实现,ModalScope最后会调用下面的buildPage抽象方法,这个buildPage抽象方法最后会调用我们一开始的widgetBuilder回调返回页面
Widget _buildModalScope(BuildContext context) {
return _modalScopeCache ??= Semantics(
sortKey: const OrdinalSortKey(0.0),
child: _ModalScope<T>(
key: _scopeKey,
route: this,
),
);
}
//来来来,接着关注这里,_buildModalScope最后会调用这个方法,但是是抽象方法,意味着又要开始往子类找了!!!一起找找这勾八是怎么构建页面的
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
}
接下来我们看看buildPage到底是哪个子类在办这事,我们跟进到了MaterialPageRoute中,但是发现MaterialPageRoute没有实现!!干!但是我看到了MaterialPageRoute定义了一个混入类MaterialRouteTransitionMixin,曙光!!我们来看看MaterialRouteTransitionMixin怎么实现buildPage方法的。
mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
//看这里
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
//我操啊,怎么还有个buildContent,往下面看,还需要被混入类来实现!!
final Widget result = buildContent(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
//MaterialPageRoute实现了该方法!!二话不说继续跟
@protected
Widget buildContent(BuildContext context);
}
class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {
//大家还记得这个吗!!就是WidgetBuilder,存放在了这个类中!!
final WidgetBuilder builder;
//实现了混合类的方法!!就是直接调用WidgetBuilder!!!把我们定义的页面返回到OverEntry中!!!!
@override
Widget buildContent(BuildContext context) => builder(context);
}
哎,设计得太好了,就是跟起来费脑,可能有一些童鞋看到这里的时候,脑内的都已经爆栈迷路了。但是没关系,我们已经知道了install是如何创建OverlayEntry的,也知道了OverlayEntry中是如何指向到我们的WidgetBuilder的,现在我们一波退栈到install方法那边。
class _RouteEntry extends RouteTransitionRecord {
//回来了,同学们快醒醒!!
void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
//获取当前状态,根据刚刚的流程,当前状态为push
final _RouteLifecycle previousState = currentState;
route._navigator = navigator;
//刚刚讲完到这里!!!,我们知道了Route准备好了页面的Widget了!也准备好了Overlay了!
route.install();
//我们在install中并没有改变Route的状态,所以会进入该判断
if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
//状态变更会pushing
currentState = _RouteLifecycle.pushing;
routeFuture.whenCompleteOrCancel(() {
if (currentState == _RouteLifecycle.pushing) {
//等一些操作完成后,会回调到这里,意味着路由的上屏已经结束,已经是后话了
currentState = _RouteLifecycle.idle;
navigator._flushHistoryUpdates();
}
});
}
//...省略一些代码
}
}
handlerPush在准备好Overlay之后会把状态流转到pushing,此时RouteEntry已经准备好Overlay了,那么刚刚也说到,Navigator是通过Overlay来上屏的。接着Navigator中的State会进行刷新,读取OverlayEntry列表,也就是说接下来我们将要了解Navigator的构造了
class NavigatorState {
List<_RouteEntry> _history = <_RouteEntry>[];
@override
Widget build(BuildContext context) {
//省略了一些嵌套布局,
return Overlay(
key: _overlayKey,
clipBehavior: widget.clipBehavior,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],//看这里!
);
}
}
我们可以看出NavigatorState中返回了Overlay,也就说明了为什么Route不是返回Widget,而是OverlayEntry,因为NavigatorState就是通过Overlay来把页面上屏的,我们再关注initialEntries这个属性,我们传入了_allRouteOverlayEntries,我们再来看看_allRouteOverlayEntries是什么!
Iterable<OverlayEntry> get _allRouteOverlayEntries {
return <OverlayEntry>[
for (final _RouteEntry entry in _history)
...entry.route.overlayEntries,
];
}
我已经写得有些不清醒了...但是最终答案来了!!我们Route创建的OverlayEntry,最终被解包到Overlay中,然后开始了渲染流程,最终我们的Route被渲染完成并且状态流转到idle。
至此,push的整个流程完结,我们再来做个简单总结一下,push整个流程是怎么样的?
- 组装RouterEntry,创建OverlayEntry和管理状态,并且扔进history中
- OverlayEntry最终会调用我们一开始的WidgetBuild来作为其Widget
- NavigatorState将Overlay作为组件,通过路由栈中的overlayEntry来显示页面。
以上!就是push方法的整体流程啦!如果觉得对你有帮助的话请不要吝啬的点个赞和收藏文章~谢谢