Flutter 深度剖析flutter路由跳转与栈管理

Flutter 深度剖析flutter路由跳转与栈管理

前言

这篇文章我会将详细说明一下路由在跳转过程中都做了一些什么,以及路由各种跳转方式在源码中到底是如何执行的,并且会扩展一下命名路由以及路由钩子。

内容

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 pushNamed(

String routeName, {

Object? arguments,

}) {

return push(_routeNamed(routeName, arguments: arguments)!);

}

参数说明

参数

说明

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? route = widget.onGenerateRoute!(settings) as 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? _onGenerateRoute(RouteSettings settings) {

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 route = widget.pageRouteBuilder!(

settings,

pageContentBuilder,

);

return route;

}

if (widget.onGenerateRoute != null) {

return widget.onGenerateRoute!(settings);

}

return null;

}

从这就能看出来,其实它push的就是一个Route对象,现在我们回到push方法去,继续往下看

Future pushNamed(

String routeName, {

Object? arguments,

}) {

return push(Route);

}

Future push(Route route) {

_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));

return route.popped;

}

先看返回值:return 一个route.popped; 从字面意思页很容易理解,返回一个pop完成的状态。

Future get popped => _popCompleter.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? poppedRoute; // 应该在顶部活动路由上触发didPopNext的路由

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? previousPresent }) {

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? previous, required Route? previousPresent }) {

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 createOverlayEntries() {

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 extends PageRoute with MaterialRouteTransitionMixin {

/// 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 on PageRoute {

/// Builds the primary contents of the route.

@protected

Widget buildContent(BuildContext context);

@override

Widget buildPage(

BuildContext context,

Animation animation,

Animation secondaryAnimation,

) {

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 route, Route? previousRoute) {

print('did push ---------${route}-----');

RouterReportManager.reportCurrentRoute(route);

if ((route.settings.name ?? '').isNotEmpty) {

StackManager().add(route.settings.name!);

}

}

@override

void didPop(Route route, Route? previousRoute) async {

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就是我自己定义的栈管理内了。

在此,分享结束。

相关作品

欢迎访问17173魔域专区
(0755) 3656 3788

欢迎访问17173魔域专区

📅 12-30 👀 724
中头奖漫画官方
365bet体育网址

中头奖漫画官方

📅 09-29 👀 6762
肠道牵全身 养生先养肠
(0755) 3656 3788

肠道牵全身 养生先养肠

📅 02-05 👀 6978