1.背景与动机
在Navigator 2.0推出之前,Flutter主要通过Navigator 1.0和其提供的 API(如push(), pop(), pushNamed()等)来管理页面路由。然而,Navigator 1.0存在一些局限性,如难以实现复杂的页面操作(如移除栈内中间页面、交换页面等)、不支持嵌套路由以及无法满足全平台(尤其是Web平台)的新需求。因此,Flutter官方团队决定对路由系统进行改造,推出了Navigator 2.0。
2.主要特性
- 声明式API Navigator 2.0提供的声明式API使得路由管理更加直观和易于理解。开发者只需声明页面的配置信息,而无需编写复杂的导航逻辑代码。这种方式不仅减少了代码量,还提高了代码的可读性和可维护性。
- 嵌套路由 Navigator 2.0满足了嵌套路由的需求场景,允许开发者在应用中创建嵌套的路由结构。这使得应用的结构更加清晰,同时也提高了页面导航的灵活性。
- 全平台支持 Navigator 2.0提供的API能够满足不同平台(如iOS、Android、Web等)的导航需求,使得开发者能够更加方便地构建跨平台的应用。
- 强大的页面操作能力 Navigator 2.0提供了更加丰富的页面操作能力,如移除栈内中间页面、交换页面等。这些操作在Navigator 1.0中很难实现或需要编写复杂的代码,而在Navigator 2.0中则变得简单直接。
3.核心组件
- Router 在Navigator 2.0中,Router组件是路由管理的核心。它负责根据当前的路由信息(RouteInformation)和路由信息解析器(RouteInformationParser)来构建和更新UI。Router组件接收三个主要参数:1.routeInformationProvider:提供当前的路由信息;2.routeInformationParser:将路由信息解析为路由配置;3.routerDelegate:根据路由配置构建和更新UI。
- RouteInformationProvider RouteInformationProvider是一个提供当前路由信息的组件。它通常与平台相关的路由信息源(如浏览器的URL、Android的Intent等)集成,以获取当前的路由信息。
- RouteInformationParser RouteInformationParser负责将RouteInformation解析为RouteConfiguration。这个过程允许开发者根据路由信息的格式(如URL)来定义如何将其映射到应用内的路由配置。
- RouterDelegate RouterDelegate是与UI构建紧密相关的组件。它必须实现RouterDelegate接口,并提供两个主要方法: 1.build(BuildContext context):根据当前的路由配置构建UI;2.setNewRoutePath(List configuration):设置新的路由路径,并更新UI;3.Future popRoute() :实现后退逻辑。
4.简单实例
首先通过MaterialApp.router()来创建MaterialApp:
需要定义一个RouterDelegate对象和一个RouteInformationParser对象。其中根据路由配置构建和更新UI,RouteInformationParser负责将RouteInformation解析为RouteConfiguration。 RouterDelegate可以传个泛型,定义其currentConfiguration对象的类型。
其中build()一般返回的是一个Navigator对象,popRoute()实现后退逻辑,setNewRoutePath()实现新页面的逻辑。定义了一个_pages数组对象,记录每个路由的path,可以理解为是一个路由栈,这个路由栈对我们来说非常友好,在有复杂的业务逻辑时,我们可以自行定义相应的栈管理逻辑。currentConfiguration返回的是栈顶的page信息。创建一个类继承RouteInformationParser,主要的作用是包装解析路由信息,这里有一个最简单的方式,如下:
好的,接下来我们看一下调用:
非常简单,直接调用Router.of(context).routerDelegate.setNewRoutePath()即可。
到此为止,一个使用Navigator2.0的最简单的路由实例就完成了。和Navigator1.0相比,看上去繁杂了不少。但是可以根据业务需求自定义路由栈进行管理,大大的提升了灵活性。接来看我们看一下Navigator2.0是如何对路由进行实现的。
5.源码简析
我们在使用Navigator2.0时,是通过MaterialApp.router()创建的MaterialApp对象,之前章节提到过,传了RouteInformationParser和RouterDelegate这两个对象。当传递了RouterDelegate对象时,_MaterialAppState中的_usesRouter会被设置为true。
在build()时,通过WidgetsApp.router()方法创建了一个WidgetsApp对象:
在_WidgetsAppState中根据routerDelegate设置了成员变量_usesRouterWithDelegates的值:
在build()时会创建一个Router对象,其中Router继承了StatefulWidget:
在上一章节的实例中我们可得知,页面的切换都是依靠RouterDelegate对象进行的。每当切换到新的页面时,都会调用setNewRoutePath()方法,因此我们来看一下setNewRoutePath()是什么时候被调用的,有两处。第一处:
我们看看_handleRouteInformationProviderNotification的调用时机:
我们可以看到在initState()时,也就是在Router被初始化的时候由widget.routeInformationProvider来监听一些状态实现新页面的切换。我们来看一下routeInformationProvider。RouteInformationProvider在我们自己没有创建的情况下,系统会默认为我们创建一个PlatformRouteInformationProvider对象。它实际上是个ChangeNotifier。系统会监听每一帧的信号发送,调用其父类routerReportsNewRouteInformation()方法,我们看看它的实现:
其中SystemNavigator.selectMultiEntryHistory()的实现如下:
这个方法是由各个平台自行实现的。从注释中我们可得知如果是在Web平台下,它会切换成history模式,并从history stack中追踪所有的变化。在history发生变化时,会发送信号给Flutter层等待处理。SystemNavigator.routeInformationUpdated()方法是用来更新路由的,我们先不做分析。接着我们回到PlatformRouteInformationProvider,看看它什么时候会执行notifyListeners()方法:
在监听到有push路由的情况下时,会调用notifyListeners(),从而实现页面的切换。我们再来看第二处调用setNewRoutePath()的地方:
parseRouteInformationWithDependencies()方法中调用的parseRouteInformation()其实就是我们自定义RouteInformationParser来进行的实现。
看到当其与父的依赖关系被改变的时候会调用setNewRoutePath()。大概率就是App初始化的时候被调用一次。
6.根据狐友业务的Web端实践
我们的Flutter团队会承担一些运营活动的H5需求。在实现时我们对路由有如下需求:
- 可以根据业务自由的管理路由栈;
- 分享链接只能分享出去默认入口链接,不希望中间的路由链接被分享出去;
- 不管有多少个路由页面,history始终不变,在响应浏览器返回键时不响应路由栈的pop操作。
在之前使用Navigator1.0时体验并不太好,一个是不够灵活,另外还需对分享出去的链接做处理。因此我们利用Navigator2.0设计了一套新的路由:
Parser实现非常简单:
Delegate的实现如下:
其中_stack是我们的路由栈,_setting是RouteSettings,每执行一个新的路由跳转,都会创建一个RouteSettings对象并赋值给_setting,最终在插入_stack里。buildPage()的实现如下:
其中MaterialPage继承了Page。getPageChild()实现如下:
我们可以看到,在真正返回Widget时,我们并没有使用传入的name参数,而是BaseArgument的name参数,这是为什么呢?这是在于我们为了实现无论页面怎么跳转,从头到尾浏览器只保留一个history,因此我们在页面跳转时RouteSettings的name并不发生变化,通过其arguments里面的参数变化返回不同的Widget。这样在路由跳转时,其实MaterialPage由于name一直会被直接复用,从而不会创建新的MaterialPage也就不会产生history。 NavigatorUtil是由业务调用的,创建跳转方法的抽象类,提供了onJumpTo(),onReplaceAndJumpTo(),onClearStack(),onBack()四个方法供业务调用,我们可以看一下onJumpTo()的实现:
可以看到在创建RouteSettings对象时,name为RouterName.rootPage,arg时由业务传的真正的跳转页面相关的参数。我们看一下业务的调用:
我们看一下截图展示:
图片
图片
图片
在这个过程中href不会发生变化,history也不会发生变化,完全符合我们的预期。
7.总结
Flutter的Navigator 2.0引入了声明式的API,使页面路由管理更加灵活和强大。相较于Navigator 1.0,Navigator 2.0支持更复杂的路由操作,如嵌套路由和动态路由配置。它使用不可变的Page对象列表来表示路由历史,与Flutter的不可变Widgets设计理念一致。Navigator 2.0还支持命名路由,通过简单的路由名称即可实现页面跳转,大大简化了路由管理的复杂度。此外,它还提供了更丰富的路由回调和状态管理功能,使开发者能够更轻松地构建复杂的Flutter应用。