前言
这篇文章我会将详细说明一下路由在跳转过程中都做了一些什么,以及路由各种跳转方式在源码中到底是如何执行的,并且会扩展一下命名路由以及路由钩子。
内容
1、MaterialPageRoute与Navigator
MaterialPageRoute
MaterialPageRoute 是一种特定类型的路由,用于在Material Design风格的应用中创建页面转场。它负责定义如何构建页面以及转场动画。
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
参数
说明
builder
函数,展示你所需要的Widget
settings
包含路由名字,参数等
maintainState
页面跳转后,原页面的状态是否保持。
fullscreenDialog
是否与全屏弹窗性质跳转。默认是false,如果设置为true,就会从底部弹出
Navigator
flutter自带的一个类,用于管理路由的导航,它就是一个栈,从栈底开始可以一直叠加。通俗一点,就是用来做跳转页面用的。
常用方法
方法
说明
push
添加一个路由(页面)到栈顶,跳转页面
pop
移除当前所在页面,返回到上一级页面
canPop
是否可以返回到上一级页面
popAndPushNamed
移除当前所在页面,并且添加一个新的路由到栈顶
popUntil
用于从路由堆栈中移除当前页面及其之前的所有页面,直到满足特定条件
pushAndRemoveUntil
将新页面添加栈中,并移除指定条件之前的所有页面
pushNamedAndRemoveUntil
将新页面添加栈中,并移除指定条件之前的所有页面
从以上常用方法看起来特意写了pushAndRemoveUntil、pushNamedAndRemoveUntil这两个方法,他们两个的说明是一致的,但是区别在于第一个只能对普通路由所使用,而第二个是给命名路由所使用的。
上面方法只是举例了一些常用方法,有想了解其他方法的,可以自行去查看一个Navigtor类里包含的方法体。
接下来让我们来看看Navigator在跳转过程中到底做了一些什么。
Navigator.push()与Navigator.pushNamed()。一个作用于普通路由,一个作用于命名路由,我们一层一层的来说明,为什么统一说明,因为他们再后面走的逻辑是一套代码
Future
String routeName, {
Object? arguments,
}) {
return push
}
参数说明
参数
说明
routeName
路由的key,就是路由的路径或名称
arguments
传入的参数
返回值:Future
在页面返回Pop的时候,会通知跳转页,且带回返回的参数
image.png
接着往下看。在调用push方法前,优先先调用了_routeName
//当onGenerateRoute 为null的时候,直接返回null
if (allowNull && widget.onGenerateRoute == null) {
return null;
}
//创建一个RouteSettings对象
final RouteSettings settings = RouteSettings(
name: name,
arguments: arguments,
);
//再得到一个route对象
Route
if (route == null && !allowNull) {
//如果返回的路由是null的,flutter会有一个默认找不到路由的页面来容错
route = widget.onUnknownRoute!(settings) as Route
}
//返回route对象
return route;
从以上代码我们发现了两个关键点,onGenerateRoute 与 RouteSettings
关于RouteSettings,大家应该不会太陌生,在上述MaterialPageRoute中就简单的说明了一下它有哪些作用,这次就着重看一下onGenerateRoute 这块源码直接引用是跳转不到的,有需要自己看看的可以,路径flutter->src->widgets->app。
Route
final String? name = settings.name;
//首先先检测一下当前路由是否是首页路由,如果是首页路由,则直接返回
//如果不是首页,则去routes里面找
final WidgetBuilder? pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
? (BuildContext context) => widget.home!
: widget.routes![name];
if (pageContentBuilder != null) {
//找到之后,就用routeSettings和widgetBuilder生成route对象返回
final Route
settings,
pageContentBuilder,
);
return route;
}
if (widget.onGenerateRoute != null) {
return widget.onGenerateRoute!(settings);
}
return null;
}
从这就能看出来,其实它push的就是一个Route对象,现在我们回到push方法去,继续往下看
Future
String routeName, {
Object? arguments,
}) {
return push
}
Future
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
return route.popped;
}
先看返回值:return 一个route.popped; 从字面意思页很容易理解,返回一个pop完成的状态。
Future
接下来我们再来看_RouteEntry里面做了一些什么
class _RouteEntry extends RouteTransitionRecord {
_RouteEntry(
this.route, {
required _RouteLifecycle initialState,
required this.pageBased,
this.restorationInformation,
}) : assert(!pageBased || route.settings is Page),
assert(
initialState == _RouteLifecycle.staging ||
initialState == _RouteLifecycle.add ||
initialState == _RouteLifecycle.push ||
initialState == _RouteLifecycle.pushReplace ||
initialState == _RouteLifecycle.replace,
),
currentState = initialState {
// TODO(polina-c): stop duplicating code across disposables
// https://github.com/flutter/flutter/issues/137435
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: 'package:flutter/widgets.dart',
className: '$_RouteEntry',
object: this,
);
}
}
不难看出在assert里面它分别只出了一些状态,staging 、add、push、pushReplace、replace。这都是在做正在入栈
的状态,再接着我们看官方注解中的说明:
image.png
从创建_routeEntry开始,到过渡准备状态,再分四种状态树,最后回到idel,再继续分布下面树直到gc。而idel状态往上,都是准备入栈到入栈的状态,往下就是入栈以后的状态。
再来看看_pushEntry这个方法
//栈集合
final _History _history = _History();
void _pushEntry(_RouteEntry entry) {
//把当前路由添加到栈集合中
_history.add(entry);
//开始刷新栈顶数据
_flushHistoryUpdates();
_afterNavigation(entry.route);
}
在开始刷新栈顶数据这个方法中,代码量过长,我开始分片段说明
第一段:
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
_flushingHistory = true;
int index = _history.length - 1; //获取当前路由的下标、也可以解释为获取当前路由的总数
_RouteEntry? next; // 下一个路由
_RouteEntry? entry = _history[index]; //获取到当前路由
_RouteEntry? previous = index > 0 ? _history[index - 1] : null; //上一个路由
bool canRemoveOrAdd = false; // 是否有一个完全不透明的路由在顶部,以静默方式删除或添加路由
Route
bool seenTopActiveRoute = false; // 我们是否见过会获得didPopNext的路由
final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
}
后面的片段为重点,开始循环判断上面的状态流
第二段:
while (index >= 0) {
switch (entry!.currentState) {
case _RouteLifecycle.add:
//调用handleAdd方法
entry.handleAdd(
navigator: this,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
);
continue;
}
void handleAdd({ required NavigatorState navigator, required Route
route._navigator = navigator;
//调用install方法, 该方法调用后负责将它们添加到[Overlay]或从[Overlay]中移除
route.install();
//把当前状态改为adding
currentState = _RouteLifecycle.adding;
//添加到导航观察队列去
navigator._observedRouteAdditions.add(
_NavigatorPushObservation(route, previousPresent),
);
}
从第二段开始,就开始循环状态了,从add开始,然后把状态改变成adding。然后再接着第三段,很明显能猜到后续的操作都是一样。
第三段:
case _RouteLifecycle.adding:
if (canRemoveOrAdd || next == null) {
//调用didAdd方法, 此方法必须在install()方法后面调用, 让它立即添加进去。 里面的方法代码就不贴了。就是把当前状态变更为idle 然后继续循环
entry.didAdd(
navigator: this,
isNewFirst: next == null,
);
assert(entry.currentState == _RouteLifecycle.idle);
continue;
}
第四段:
case _RouteLifecycle.push:
case _RouteLifecycle.pushReplace:
case _RouteLifecycle.replace:
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,
);
if (entry.currentState == _RouteLifecycle.idle) {
continue;
}
void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route
final _RouteLifecycle previousState = currentState;
route._navigator = navigator;
route.install();
if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
final TickerFuture routeFuture = route.didPush();
currentState = _RouteLifecycle.pushing;
routeFuture.whenCompleteOrCancel(() {
if (currentState == _RouteLifecycle.pushing) {
currentState = _RouteLifecycle.idle;
navigator._flushHistoryUpdates();
}
});
} else {
route.didReplace(previous);
currentState = _RouteLifecycle.idle;
}
if (isNewFirst) {
route.didChangeNext(null);
}
if (previousState == _RouteLifecycle.replace || previousState == _RouteLifecycle.pushReplace) {
navigator._observedRouteAdditions.add(
_NavigatorReplaceObservation(route, previousPresent),
);
} else {
navigator._observedRouteAdditions.add(
_NavigatorPushObservation(route, previousPresent),
);
}
}
在片段二的时候,就简单的说明了一下它的拿来做什么的。那么在这里我将详细的说明一下install(),该方法给之前的那个onGenerateRoute方法实现都不好找,我现在把路径写出来,有想看的可以根据路径找
flutter->materialMaterial->page->MaterialPageRoute继承PageRoute 继承 ModalRoute继承 TransitionRoute 继承OverlayRoute
void install() {
//overlayEntries可以说是渲染队列,渲染的就是页面。在install的方法中,相当于把当前overlayEntries添加到队列去了
_overlayEntries.addAll(createOverlayEntries());
super.install();
}
接着我们来看ModalRoute里面的createOverlayEntries方法
late OverlayEntry _modalBarrier;
late OverlayEntry _modalScope;
Iterable
return
//绘制灰色蒙层、 举例:我们在使用dialog的时候,弹出时,在空白页后面会有一层灰色蒙层。用的就是它
//该方法的官方说明如下,在此不做过多说明。
///// Build the barrier for this [ModalRoute], subclasses can override
/// this method to create their own barrier with customized features such as
/// color or accessibility focus size.
///
/// See also:
/// * [ModalBarrier], which is typically used to build a barrier.
/// * [ModalBottomSheetRoute], which overrides this method to build a
/// customized barrier.
_modalBarrier = OverlayEntry(builder: _buildModalBarrier),
// 我们主要来看_buildModalScope方法
_modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState, canSizeOverlay: opaque),
];
}
// one of the builders
Widget _buildModalScope(BuildContext context) {
// To be sorted before the _modalBarrier.
return _modalScopeCache ??= Semantics(
sortKey: const OrdinalSortKey(0.0),
child: _ModalScope
key: _scopeKey,
route: this,
// _ModalScope calls buildTransitions() and buildChild(), defined above
),
);
}
到此,有没有发现终于有返回Widget的了,这个方法就是返回具体的Widget了。在_ModalScope这个StatefulWidget中,最终会调用一个buildPage()抽象类,到了这一步,我们再回到MaterialPageRoute中,其中有个参数是WidgetBuilder,它就是拿来展示你所需要的Widget。那么我们就开始去MaterialPageRoute中找buildPage()
class MaterialPageRoute
/// Construct a MaterialPageRoute whose contents are defined by [builder].
MaterialPageRoute({
required this.builder,
super.settings,
this.maintainState = true,
super.fullscreenDialog,
super.allowSnapshotting = true,
super.barrierDismissible = false,
}) {
assert(opaque);
}
/// Builds the primary contents of the route.
final WidgetBuilder builder;
@override
Widget buildContent(BuildContext context) => builder(context);
@override
final bool maintainState;
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
从上面代码片段可以分析出,MaterialPageRoute中必传的builder 是从buildContent方法中生成的。但是在继承的PageRoute类里面没有定义buildContent方法,那就只能是混入了MaterialRouteTransitionMixin。
mixin MaterialRouteTransitionMixin
/// Builds the primary contents of the route.
@protected
Widget buildContent(BuildContext context);
@override
Widget buildPage(
BuildContext context,
Animation
Animation
) {
final Widget result = buildContent(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
到了这一步就已经很明朗了,最终返回一个Widget给到了MaterialPageRoute来做展示页面。
2、命名路由
命名路由,顾名思义就是给你的路由起个名字,这样就方便路由管理了。 每个路由都会有自己的名字,可以通过名字去进行导航跳转,而无需直接去写构建路由的路径
路由列表
在里面定义了所有可操作的路由页面,优点:
简化导航
集中管理
提高代码可读性
在我自己的项目中用的是GetX,所以展示一个GetX的路由列表
static final routes = [
GetPage(
name: AppRoutes.Login,
page: () => LoginPage(),
),
GetPage(
name: AppRoutes.Proxy,
page: () => ProxyPage(),
),
GetPage(
name: AppRoutes.Home,
page: () => HomePage(),
),]
注册路由列表
return GetMaterialApp(
getPages: AppPages.routes,
)
3.路由钩子
注意:onGenerateRoute 这个方法只针对命名路由。 这也是为什么我把命名路由放在前面说明。
使用场景:假设一个APP的需求,是需要游客模式的,也就是说,在某些页面是可以不需要登录就能进去预览的。这个时候就可以使用路由钩子了。
MaterialApp( //如果使用了GetX 就把MaterialApp 换成GetMaterialApp
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
//该routeName 为当前页面的路由名称,可以在下面做一切的逻辑判断,比如
// 如果跳转的路由需要登录,但当前未登录,则直接返回登录页,
// 其它情况则正常打开路由。
}
);
}
);
4.栈管理(扩展)
想法:我现在有一个需求,栈里面有5个路由,我要关闭第2、第4、第5个路由,保留第1和第3路由怎么办。
根据之前讲的路由跳转,可以使用条件约束跳转pushNamedAndRemoveUntil,第一个参数填写第3个路由名称,第二个参数条件去等于第1个路由页面。这样确实可以做到,但是第3个路由会重载。那不重载怎么做,是不是就要多次组合去调用路由跳转。又或者有更复杂的跳转需求呢?
想法:根据原生开发的经验,我们一般都会定义一个栈管理,在每个activity初始化的实际,去把路由添加到栈内,然后在销毁的时候把当前的activity从栈内移除,接着在栈管理写出各种功能方法来控制栈的出入。同理,flutter也是如此。我在看flutter源码中,目前还没有找到能够准确拿到栈列表的函数,源码里面的栈列表都属于私有的,在应用层上获取不到。后续我会继续去了解一下。也希望有大牛能直接点出该方法。
现把我当前实现的说明一下。在说跳转管理的时候,有提到过在每次add栈的时候,都会有一个观察者在观察。那在源码中我也找到了NavigatorObserver这个类。
class RouterObserver extends NavigatorObserver {
@override
void didPush(Route
print('did push ---------${route}-----');
RouterReportManager.reportCurrentRoute(route);
if ((route.settings.name ?? '').isNotEmpty) {
StackManager().add(route.settings.name!);
}
}
@override
void didPop(Route
print('did pop ------${route}--------');
if ((route.settings.name ?? '').isNotEmpty) {
StackManager().remove(route.settings.name!);
}
RouterReportManager.reportRouteDispose(route);
}
}
初始化RouterObserver
MaterialApp( //如果使用了GetX 就把MaterialApp 换成GetMaterialApp
navigatorObservers: [RouterObserver ()],
}
);
在NavigatorObserver 内部需实现两个方法,didPush,didPop。这两个方法都代表立即执行,当用新的栈添加时,会调用didPush,当有页面要移除栈时,会调用didPop。而StackManager就是我自己定义的栈管理内了。
在此,分享结束。