Flutter 路由管理源码解析

一、介绍:

路由在移动开发中通常指页面,路由管理就指的是管理页面之间的跳转,在flutter中,是通过一个栈的结构来对路由进行维护管理。路由管理也就是说在代码层面其实是对这个栈结构进行管理,其中路由入栈操作就相当于打开一个新的页面,放入这个栈结构中,路由出栈操作对应的是页面关闭操作,但是当一个APP中使用到页面很多时,如何更加有效合理的去管理各个页面之间的关系和跳转逻辑将变得极为重要。

**路由:**在flutter中,一个路由中维护了与之相关的路由状态,路由页,以及该路由对应的标签数据以及指针位置等内容。

**路由栈:**在我们的flutter项目中,页面的路由是通过一个栈结构进行管理并维护的,而栈的特点就是只能从栈顶位置进行插入内容和取出内容,当我们打开一个新的页面时,其实就是向这个栈结构中从顶部放入一个新的路由,随着打开的路由页逐渐变多,最先插入的路由将会逐渐被压入栈的底部,当我们从某个页面返回到上一页的时候,也是从栈顶位置pop出路由。

在这里插入图片描述
每一个存放在栈中的路由都存在2个指针,一个指向下一个路由next(在栈内位于当前路由之上的路由),一个指向上一个路由(在栈内位于当前路由之下的路由),通过栈的方式存储路由,通过链的方式维护路由之间的关系。

在这里插入图片描述
1、在flutter 中我们通过Navigator来完成对路由的操作

例:

//打开一个新的页面

Navigator.push(context,
    MaterialPageRoute(builder: (BuildContext context) {
  return SecondPage();
}));

//返回上一个页面

Navigator.pop(context);

2、在flutter中除了最简单基本的打开新的页面与返回上一页之外还提供了其他方法来完成更加复杂的操作

pushReplacement:替换当前页面,并且当新页面动画执行完成之后dispose前一个页面

例:

Navigator.pushReplacement(context,
  MaterialPageRoute(builder: (BuildContext context) {
    return ThirdPage();
  },),result: 'second page');

newRoute:要打开的新路由页

Result:第三个可选参数result表示的是这个页面的返回结果,如果设置的话,会返回给被替换的这个页面的前一个页面。

pushAndRemoveUntil:跳转到指定页面,并按顺序(从栈顶到栈底)移出之前的所有页面,直到predicate(指定页面)返回true

例:

Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
  builder: (BuildContext context) {
    return ThirdPage();
  },
), (router) {
//  flutter中root页settings.name默认为"/"
  return router.settings.name == "/";
});

Context:上下文对象

newRoute:新路由页

Predicate:移除Predicate之上的的所有页面

例如从Page2调用跳转pushAndRemoveUntil到Page3,同时指定predicate的条件为route.settings.name == “/”,那么跳转到Page3后Page2将被移除,因为第一个页面的默认RouteSetting的name属性值为"/"。

如果predicate的条件为route.settings.name != “/”,那么任何一个页面都不会被移除,因为判断第一个前页面Page2的时候predicate已经返回true。

popUntil:按顺序从栈内移除最顶上的页面,直到predicate返回true

例:

Navigator.popUntil(context, (router) {
  return router.settings.name == '/';
});

Context:上下文对象

Predicate:移除Predicate之上的的所有页面

3、数据的传递

将数据传递给下一个页面

非命名路由直接定义变量,直接使用路由的构造函数传参即可。

//在新页面的构造方法中接收上一个页面传递过来的数据

class SecondPage extends StatefulWidget {
  var firstPageMsg;

  SecondPage({this.firstPageMsg='星期天'});

  @override
  _SecondPageState createState() => _SecondPageState();
}

//在push方法中的PageRoute将需要传递的数据直接通过page的构造方法传入即可,理解不困难。

Navigator.push(
        context,
    MaterialPageRoute(
            builder: (BuildContext context) {
              return SecondPage(firstPageMsg: '星期一',);
            },
            settings: RouteSettings(name: 'firstPage')))
    .then((value) {
      print('接收返回数据:$value');
});

2.将数据返回给上一页

使用pop方法,通过他的result将数据进行返回。

Navigator.pop(context, 'second page message');

@optionalTypeArgs
static void pop<T extends Object>(BuildContext context, [ T result ]) {
  Navigator.of(context).pop<T>(result);
}

push方法会返回一个future,

@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}

所以只需要通过then关键字就可以接收到返回的数据

Navigator.push(
        context,
        MaterialPageRoute(
            builder: (BuildContext context) {
              return SecondPage(
                firstPageMsg: '星期一',
              );
            },
            settings: RouteSettings(name: 'firstPage')))
    .then((value) {
  print('接收返回数据:$value');
});

4、命名路由

适用于应用中页面比较多的情况,减少代码重复,方便统一进行管理。

使用命名路由:

1.提供一个路由集放入MaterialApp下的routes属性

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      routes: {
        'FirstPage': (context) => FirstPage(),
        'SecondPage': (context) => SecondPage(),
        'ThirdPage': (context) => ThirdPage(),
      },

//可以在onUnknownRoute中设置一个路由跳转错误页面
      onUnknownRoute: (RouteSettings setting) =>
          MaterialPageRoute(builder: (context) => UnknownPage()),
      home: MyHomePage(),
    );
  }
}

2.使用pushNamed(context, ‘XXXPage’)即可完成页面的跳转,通过arguments参数可以进行数据的传递

Navigator.pushNamed(context, 'SecondPage',arguments:'first page msg' ).then((value){

});

3.除此之外,命名路由还提供了与非命名路由相对应的其他几种跳转方式如:
pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil

三者分别对应push、pushReplacement、pushAndRemoveUntil,路由的名字将会传递给Navigator的onGenerateRoute回调,并将返回的路由推入Navigator栈

例:

Navigator.pushReplacementNamed(context,'ThirdPage',result: 'second back msg',arguments: 'second next msg');

5、PageRoute路由导航

当我们在使用Navigator进行路由操作时,必定要传入PageRoute对象,PageRoute定义了路由的实现风格与具体的路由页。

Navigator.push(context,
    MaterialPageRoute(builder: (BuildContext context) {
      return SecondPage();
    },fullscreenDialog:true)).then((value){
      
});

PageRoute默认提供了三个公开的实现类:

CupertinoPageRoute:Cupertino风格的默认实现。//IOS风格的实现
MaterialPageRoute:Material风格的默认实现。//android 风格的实现
PageRouteBuilder:自定义PageRoute,实现自定义风格效果

看一下MaterialPageRoute的构造方法

MaterialPageRoute({
  @required this.builder,
  RouteSettings settings,
  this.maintainState = true,
  bool fullscreenDialog = false,
}) : assert(builder != null),
     assert(maintainState != null),
     assert(fullscreenDialog != null),
     assert(opaque),
     super(settings: settings, fullscreenDialog: fullscreenDialog);

此对象有4个可选参数:

Builder:是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,我们通常要实现此回调,返回新路由页的实例。

settings 包含路由的配置信息,我们可以通过settings来对路由的名称进行设置,以及设置传递给下一个页面的参数

class RouteSettings {
  /// Creates data used to construct routes.
  const RouteSettings({
    this.name,   //设置路由name属性
    this.arguments, //传递参数
  });

maintainState:当页面完全不可见时是否还继续保持页面的状态,默认状态为true,除此之外,当触发navigator.pop时与hero页面过度动画有关联。

fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,这个参数会影响到一些如导航栏转换,动画处理,ios返回手势的处理,appbar默认close或back按钮的显示等处理。

CupertinoPageRoute只是和MaterialPageRoute的实现风格不同,具体内部逻辑相似。

PageRouteBuilder的构造方法:

PageRouteBuilder({
  RouteSettings settings,
  @required this.pageBuilder,

//过渡动画构造器,路由的主变换动画,如果是进入,值从0.0逐渐变化到1.0;如果是退出,值从1.0逐渐变化到0.0
this.transitionsBuilder = _defaultTransitionsBuilder,

//进入下一页动画持续的时间
this.transitionDuration = const Duration(milliseconds: 300),

//返回上一页动画持续时间
this.reverseTransitionDuration = const Duration(milliseconds: 300),

//是否不透明,默认为true,如果是不透明的话,路由变换完成之后,不会再构建位于该路由之下//的路由,以节省资源。
this.opaque = true,

//点击屏障是否自动消失,一般和dialog弹窗有关系
this.barrierDismissible = false,

//页面间屏障颜色,如果为null,则屏障将是透明的
this.barrierColor,

//页面间屏障标签
this.barrierLabel,

//是否保持页面状态
this.maintainState = true,

//是否为全屏对话框

  bool fullscreenDialog = false,
}) : assert(pageBuilder != null),
     assert(transitionsBuilder != null),
     assert(opaque != null),
     assert(barrierDismissible != null),
     assert(maintainState != null),
     assert(fullscreenDialog != null),
     super(settings: settings, fullscreenDialog: fullscreenDialog);

**

二、Flutter路由原理

**

我们可以将整个路由的过程分为以下几个阶段来观察

在这里插入图片描述

1、PageRoute的工作过程:
例:

MaterialPageRoute(builder: (BuildContext context) {
  return SecondPage();
}

1.进入到MaterialPageRoute的构造方法,可以看出这里必须要传入一个必传参数builder,它会返回一个新的页面。

MaterialPageRoute作用:

1.存放传入的RouteSettings

2.设置maintainState标记

3.设置fullscreenDialog标记

4.传递Page对象

在这里插入图片描述

2.当buildContent方法被调用的时候会去调用 builder,从而返回一个widget

在这里插入图片描述

3.buildContent此方法是MaterialRouteTransitionMixin中的方法,MaterialRouteTransitionMixin是MaterialPageRoute的混合对象

MaterialRouteTransitionMixin作用:

1.设置路由切换风格效果(动画时长/间隔颜色等)

2.对page进行语义化处理

在这里插入图片描述

4.MaterialRouteTransitionMixin的buildPage方法中会触发buildContent,并在最终通过Semantics进行包装并返回。
在这里插入图片描述

Semantics(语义) 用于描述Widget的含义最终达到描述应用程序的UI。这些描述可以通过辅助工具、搜索引擎和其他语义分析软件使用。它有点像HTML5的语义元素,在Android、iOS上更多是用于读屏,帮助一些有视力障碍的人使用我们的软件(Android TalkBack 和 iOS VoiceOver)。

5.继续追踪buildPage,可以发现此方法通过routes的_ModalScope 中被构建出来的build方法中被调用。

_ModalScope作用:

1.加载动画效果(进入or退出动画)

2.页面焦点获取与释放

3.根据自定义pageroute属性对widget进行处理

buildPage是MaterialRouteTransitionMixin中的方法而MaterialRouteTransitionMixin继承于PageRoute,PageRoute继承于ModalRoute,

以下的操作都会在ModalRoute中进行。
在这里插入图片描述

在这里插入图片描述

6.查看_ModalScope 在什么地方做了构建就可以理解,我们路由的新页面在何时被加载出来。

可以看到_buildModalScope实例化的时候_ModalScope 作为其child也会被实例化。
在这里插入图片描述
7.根据调用关系,可以发现在createOverlayEntries集合中,将buildModalScope封装为了OverlayEntry然后完成实例化,OverlayEntry是一个叠加组件,所以这里就可以明白,

当我们跳转到一个新的页面时,其内部逻辑是通过将新的页面封装成一个OverlayEntry从而覆盖在之前的页面之上最终将UI呈现在屏幕上。
在这里插入图片描述

OverlayEntry:叠加组件,可以悬浮在其他组件上面。

8.createOverlayEntries是ModalRoute中的方法,createOverlayEntries会在install方法被调用时添加到overlayEntries集合中去。

OverlayRoute是ModalRoute的父类
在这里插入图片描述

9.而install方法是ModalRoute的父类OverlayRoute中的方法。而该OverlayRoute的实现子类会在Navigator的相关方法中作为route参数传入,并在合适的位置调用install方法。

例:push
在这里插入图片描述

到此为止我们已经完成了对PageRoute的工作工程的梳理,以下为整个工作过程

在这里插入图片描述

2、Navigator工作过程

在我们使用Navigator方法进行路由操作时,此时PageRoute对象已经完成了构建并且传递到了Navigator内部

在这里插入图片描述
继续以这个为例:

Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return SecondPage();
}));

1.进入到Navigator.push方法,可以看到其内部是回调了以下方法

在这里插入图片描述

在这里插入图片描述
2.深入查看of中做了什么,可以看到主要做了这么几个操作,一个是对navigator做空判断,如果未空则抛出异常。还有一个则是对rootNavigator进行判断,是否为根navigator(根navigator会在WidgetsApp中初始化),如果是则调用findRootAncestorStateOfType负责调用findAncestorStateOfType完成navigator 的实例化。
在这里插入图片描述
3.分别分析上述2个方法做了哪些操作。

//Returns the [State] object of the nearest ancestor [StatefulWidget] widget

findAncestorStateOfType

通过官方注释可以看到,该方法可以从当前节点沿着 widget 树向上查找指定类型的最近的StatefulWidget 对应的 State 对象。

//Returns the [State] object of the furthest ancestor [StatefulWidget] widget

findRootAncestorStateOfType

通过官方注释可以看到,该方法返回了最远widget树指定类型的StatefulWidget 对应的State对象。

state的作用:将NavController与路由导航之间共享的状态做了封装,NavController负责所有路由导航的栈处理是全局性质的。

4.继续回到Navigator.of(context).push(route),当我们做完of逻辑之后,此处返回了对应的state对象完成了接下来的push操作。在push中,可以看到此处将route与路由类型RouteLifecycle封装为了RouteEntry对象并添加到了history中,这个history是一个list集合,也就是这个history实际就是我们一直说的路由栈,所以在flutter中路由栈的表现方式是通过history这个list集合来完成的。

以push方法为例,当我们做push操作时,会将route封装为RouteEntry并将状态置为push,然后插入到history中去,所以这个操作很明白的就可以看出相当于是将我们新插入的页面放入到了history这个栈中进行了存放。

_debugLocked:路由锁,防止在路由操作的过程中,对路由进行并发操作导致管理混乱

在这里插入图片描述
5.这个RouteEntry类很重要,其内部主要做了2件事,第一个是根据widget的不同状态对路由RouteLifecycle的状态进行变更,第二个是根据_RouteLifecycle的不同状态做对应的逻辑处理。

在这里插入图片描述
6.继续查看push方法的flushHistoryUpdates,这里是路由操作的核心逻辑,此处使用到了状态模式+双向链表,对路由栈中存放的路由进行遍历,并根据状态做不同的操作处理及状态变更,并对栈中的路由关系进行了确认。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
根据代码的调用逻辑,我们分析完成while循环中做了什么事情后,先不着急分析之后的逻辑,我们从while循环中进行,深入分析下不同状态下RouteEntry到底做了什么事情和操作。

7.handleAdd:在这个方法中,我们可以看到,我们在push中传入的route对象在这里触发了install方法,也正好和我们分析routePage的逻辑发生了对应,到此就豁然开朗,整个调用链逐渐变的清晰起来。并且当我们调用完install方法后会将当前路由节点的状态置为adding。并调用 navigator._observedRouteAdditions方法将route与路由栈中当前节点的上一个节点previousPresent封装为了NavigatorPushObservation然后添加到了observedRouteAdditions中。

在这里插入图片描述
8.didAdd:当我们的路由状态变为adding后,会去调用entry.didAdd在这个方法内部,调用了传递进来的route的didAdd方法,并在调用结束后将状态改变为idle闲置状态,如果当前路由为最新插入路由栈的新路由则还会去调用didChangeNext方法。
在这里插入图片描述
在这里插入图片描述
9.我们查看didAdd在route的父类ModalRoute中的实现。

在这里看到首先会对scopeKey.currentState做一个非空校验,这个scopeKey其实是一个GlobalKey,

GlobalKey:每个globalkey都是一个在整个应用内唯一的key,globalkey相对而言是比较昂贵的.

作用:

1.允许widget在应用程序中的任何位置更改其parent而不丢失其状态。应用场景:在两个不同的屏幕上显示相同的widget,并保持状态相同

2.globalkey唯一定义了某个element,它使你能够访问与element相关联的其他对象,例如buildContext、state等。应用场景:跨widget访问状态:

那么在这里我们就利用了globalkey对状态进行判断处理。

在这里插入图片描述
setFirstFocus其实可以理解为将我们的焦点设置为当前路由页
在这里插入图片描述
我们查看didChangeNext在route的父类TransitionRoute

中的实现,其核心就是调用了_updateSecondaryAnimation方法,这个方法就是用来执行我们路由切换时的动画效果。如果我们使用了自定义风格路由,则设置的自定义动画效果将会在这里执行。这一步涉及到了操作系统底层的相关操作,例如页面的重新渲染等

在这里插入图片描述

10.当路由状态为adding时,会将当前焦点设置在新放入的路由页,然后将路由页的当前状态设置为idle,并且执行路由跳转切换动画。我们继续看idle状态操作

在这里会判断顶部路由状态是否活跃以及poppedRoute 是否为空,符合条件则调用handleDidPopNext方法。并在最后将seenTopActiveRoute 与canRemoveOrAdd 都设置为了\

在这里插入图片描述
handleDidPopNext,内部调用了route.didPopNext方法,参数为被pop掉的route,最后对释放掉的route记录在lastAnnouncedPoppedNextRoute 中(最近被释放掉的route)

在这里插入图片描述
didPopNext,执行路由页切换跳转动画
在这里插入图片描述
11.接下来我们看push、pushReplace、replace这三个状态的操作。在这里调用了handlePush方法

在这里插入图片描述
进入handlepush
在这里插入图片描述
在这里插入图片描述
我们分析一下didReplace,在这个方法中,只是将动画控制器转移给了新的route.

在这里插入图片描述
12.查看路由状态为pushing,这一步调用了entry.handleDidPopNext方法,并在最后将栈顶路由状态活跃标记seenTopActiveRoute 置为true.
在这里插入图片描述

13.我们接下来看POP状态,核心逻辑为entry.handlePop
在这里插入图片描述
handlePop主要是将当前路由状态置为popping,对于POP事件来说,当状态被置为poping时,此时还在进行动画过程,等动画过程完成后会将状态置为dispose,这个过程涉及到了底层消息的通知等过程

在这里插入图片描述
其他几个状态的逻辑都类似不继续做分析

14.我们跳出while循环继续分析_flushHistoryUpdates接下来的内容

_flushRouteAnnouncement刷新路由状态,对栈中存放的路由自顶向下遍历,重新对节点的上下游进行梳理

在这里插入图片描述
15.我们再看一下pushAndRemoveUntil的具体实现,可以发现在这个方法内基本都是对history路由栈做了操作,和push操作逻辑大部分一致,然后对这个栈从顶向下做遍历调用其remove方法。
在这里插入图片描述
16.在这个remove方法内主要是将对应栈中的路由的的状态置为了remove,然后继续触发flushHistoryUpdates根据状态做接下里的处理。
在这里插入图片描述
3、参数传递过程
1.在我们push方法结束后会返回route.popped
在这里插入图片描述
而popped 返回的就是_popCompleter.future
在这里插入图片描述
2.当我们在调用pop方法进行结果返回时,则会调用didComplete方法将结果传递给上一个页面
在这里插入图片描述
3.当我们使用命名路由时使用以下的方式进行参数传递,进到代码内部
在这里插入图片描述
在这一步会将传递进来的arguments存放入到RouteSettings中去,然后在新的页面就可以通过
在这里插入图片描述

通过ModalRoute.of(context).settings.arguments 就可以拿到我们传递的数据了
到此Navigator的处理逻辑就可以整理为以下结构。
在这里插入图片描述

三、第三方路由插件了解

fluro:

1.层次分明,条理化,
2.扩展性很强,同时也便于整体管理路由,控制跳转。
3.用uri 的方式可以统一协议,方便混合或者是多端开发。

使用:

https://pub.flutter-io.cn/packages/fluro

GetX:

1.GetX全家桶之一,使用方便,有完善的生态
2.方法调度简单
使用:

https://pub.flutter-io.cn/packages/get#more-details-about-route-management

FlutterBoost:
1.轻松地为现有原生应用程序提供Flutter混合集成方案,可以实现flutter与natvie页面的管理。

使用:https://pub.flutter-io.cn/packages/flutter_boost


版权声明:本文为qq_24856205原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。