Flutter中go_router路由管理的使用指南
作者:岛上码农
前言
go_router
是一个 Flutter 的第三方路由插件,相比 Flutter 自带的路由,go_router
更加灵活,而且简单易用。在 App 应用中,如果你想自己控制路由的定义和管理方式,那么它将十分有用。同时,对于 Web 应用来说,go_router
也提供了很好的支持。 使用 go_router
后,你可以定义 URL 的格式,使用 URL 跳转,处理深度链接以及其他一系列的导航相关的应用场景。
GoRouter 特性
GoRouter
针对页面导航提供了下面这些特性:
- 使用模板语法解析路由路径和路由查询(
query
)参数; - 支持单个目标路由展示多个页面(子路由);
- 重定向:可以基于应用状态跳转到不同的URL,比如用户没有登录时跳转到登录页;
- 使用
StatefulShellRoute
可以支持嵌套的 Tab 导航; - 同时支持
Material
风格和Cupertino
风格应用; - 兼容
Navigator
API 。
添加插件
当前最新版本的 go_router
是10.0.0(6.3.0版本以上需要 Dart 2.18),可以根据自己的需要添加相应的版本。在 pubspec.yaml 中加入依赖的版本即可,下面是以7.1.1版本为例。
dependencies: go_router: ^7.1.1
路由配置
引入 go_router
插件后,就可以在应用中配置 GoRouter
,代码如下:
import 'package:go_router/go_router.dart'; // GoRouter configuration final _router = GoRouter( initialLocation: '/', routes: [ GoRoute( name: 'home', // Optional, add name to your routes. Allows you navigate by name instead of path path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( name: 'page2', path: '/page2', builder: (context, state) => Page2Screen(), ), ], );
然后,我们就可以通过MaterialApp.router
或CupertinoApp.router
构造函数来使用 GoRouter
,并且将 routerConfig
参数设置为我们前面定义的 GoRouter
配置对象。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, ); } }
接下来就可以愉快地玩耍 GoRouter
了。
路由参数
GoRouter
的每一个路由都通过 GoRoute
对象来配置,我们可以在构建 GoRoute
对象时来配置路由参数。路由参数典型的就是路径参数,比如 /path/:{路径参数}
,这个时候 GoRoute
的路径参数和很多 Web 框架的路由是一样的,通过一个英文冒号加参数名称就可以配置,之后我们可以在回调方法中通过 GoRouterState
对象获取路径参数,这个参数就可以传递到路由跳转目的页面。
GoRoute( path: '/fruits/:id', builder: (context, state) { final id = state.params['id'] // Get "id" param from URL return FruitsPage(id: id); }, ),
同样,也可以从GoRouterState
中获取 URL 路径中的查询(query)参数,例如下面的代码就是从/fruits?search=antonio
中获取search
参数。
GoRoute( path: '/fruits', builder: (context, state) { final search = state.queryParams['search']; return FruitsPage(search: search); }, ),
添加子路由
路由匹配后可以支持多个页面(即子路由),当一个新的页面在旧的页面之上展示时,这个时候的效果和调用 push
方法是一样的,。如果页面提供了 AppBar 组件的话,那么会自动增加返回按钮。 要使用子路由,我们只需要在上级路由中增加对应的下级路由即可,代码如下。
GoRoute( path: '/fruits', builder: (context, state) { return FruitsPage(); }, routes: <RouteBase>[ // Add child routes GoRoute( path: 'fruits-details', // NOTE: Don't need to specify "/" character for router's parents builder: (context, state) { return FruitDetailsPage(); }, ), ], )
页面导航
GoRouter 提供了多种方式跳转到目的页面,比如使用context.go()
跳转到指定的 URL 地址。
build(BuildContext context) { return TextButton( onPressed: () => context.go('/fruits/fruit-detail'), ); }
也可以使用路由的名称进行跳转,这个时候调用context.goNamed()
即可。
build(BuildContext context) { return TextButton( // remember to add "name" to your routes onPressed: () => context.goNamed('fruit-detail'), ); }
如果要构建查询参数,那么可以使用 Uri 类来构建路由路径。
context.go( Uri( path: '/fruit-detail', queryParameters: {'id': '10'}, ).toString(), );
如果要从当前页面返回的话,调用context.pop()
即可。
嵌套导航
有些应用在同一个页面展示多个子页面,例如 BottomNavigationBar
在进行导航的时候就可以一直保留在屏幕底部。这种其实是嵌套导航,在 GoRouter
里,可以通过StatefulShellRoute
来实现。 StatefulShellRoute
不直接使用根导航(root Navigator),而是通过不同的导航的子路由来实现嵌套导航。对于每一个导航分支,都会创建各自独立的导航,相当于是一个并行导航树,从而实现了有状态的嵌套导航。 当我们使用BottomNavigationBar
的时候,就可以为很方便地为每一个 Tab 配置一个持久的导航状态。 StatefulShellRoute
通过指定一个StatefulShellBranch
类型的列表来完成,列表每一个元素代表路由树的一个独立的有状态的分支。StatefulShellBranch
为每个分支提供了根路由和 Navigator
key(GlobalKey
),并提供了可选的初始默认路由地址。 我们来看看具体怎么实现。 首先创建我们的 GoRouter
对象,这个时候我们需要添加StatefulShellRoute.indexedStack
到路由中,这个类负责创建嵌套路由。StatefulShellRoute.indexedStack() 实际上是使用了 IndexedStack
创建了一个StatefulShellRoute
。 这个构造函数使用IndexedStack
来管理每个分支导航的页面,示例代码如下:
// Create keys for `root` & `section` navigator avoiding unnecessary rebuilds final _rootNavigatorKey = GlobalKey<NavigatorState>(); final _sectionNavigatorKey = GlobalKey<NavigatorState>(); final router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/feed', routes: <RouteBase>[ StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) { // Return the widget that implements the custom shell (e.g a BottomNavigationBar). // The [StatefulNavigationShell] is passed to be able to navigate to other branches in a stateful way. return ScaffoldWithNavbar(navigationShell); }, branches: [ // The route branch for the 1º Tab StatefulShellBranch( navigatorKey: _sectionNavigatorKey, // Add this branch routes // each routes with its sub routes if available e.g feed/uuid/details routes: <RouteBase>[ GoRoute( path: '/feed', builder: (context, state) => const FeedPage(), routes: <RouteBase>[ GoRoute( path: 'detail', builder: (context, state) => const FeedDetailsPage(), ) ], ), ], ), // The route branch for 2º Tab StatefulShellBranch(routes: <RouteBase>[ // Add this branch routes // each routes with its sub routes if available e.g shope/uuid/details GoRoute( path: '/shope', builder: (context, state) => const ShopePage(), ), ]) ], ), ], );
在上面的代码中,我们在路由中加入了StatefulShellRoute.indexedStack()
,由它负责创建路由分支以及返回一个自定义的导航壳,这里是BottomNavigationBar
。
- 在 builder 参数中, 我们返回导航用的壳,一个简单的带有
BottomNavigationBar
的Scaffold
,这里需要记得将navigationShell
传给页面,因为我们需要用它来导航到其他分支,例如从Home到 Shope。 - 在路由分支数组
branches
中,我们提供了一个StatefulShellBranch
的数组。这里只需要给第一个元素的navigatorKey
提供之前创建的全局的_sectionNavigatorKey
。其他分支则使用默认的 key。同时,为每个分支提供了一个RouteBase
列表,该列表是对应分支的路由。
下面是我们定义的带有BottomNavigationBar
的自定义导航壳的代码。
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class ScaffoldWithNavbar extends StatelessWidget { const ScaffoldWithNavbar(this.navigationShell, {super.key}); /// The navigation shell and container for the branch Navigators. final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, bottomNavigationBar: BottomNavigationBar( currentIndex: navigationShell.currentIndex, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shope'), ], onTap: _onTap, ), ); } void _onTap(index) { navigationShell.goBranch( index, // A common pattern when using bottom navigation bars is to support // navigating to the initial location when tapping the item that is // already active. This example demonstrates how to support this behavior, // using the initialLocation parameter of goBranch. initialLocation: index == navigationShell.currentIndex, ); } }
在上面的代码中,实际上就是构建带有BottomNavigationBar
的Scaffold
然后 body 是从路由里获取的navigationShell
. 路由分支及页面的切换通过_onTap(index)
实现,当点击某个 Tab 时,就使用navigationShell.goBranch(index)
来完成切换动作。完整代码:flutter-go_router-with-nested-tab-navigation。
路由守卫(Guards)
有些路由地址需要守卫,例如对于没有登录的用户,有些页面就无法访问。GoRouter
可以设置全局的重定向路由。最常见的一个场景就是对于没有登录的用户,跳转到/login
登录页面。 在 GoRouter 中,可以通过redirect
参数配置重定向,这是一个GoRouterRedirect
的回调方法。如果要基于应用状态更改跳转都只,那么既可以在GoRouter
或GoRoute
的构造方法中增加redirect
参数。其中 GoRoute
是只针对当前路由进行跳转处理,而GoRouter
这是全局处理。下面是示例代码,如果不需要跳转,则在回调方法中返回 null
即可。
GoRouter( redirect: (BuildContext context, GoRouterState state) { final isAuthenticated = // your logic to check if user is authenticated if (!isAuthenticated) { return '/login'; } else { return null; // return "null" to display the intended route without redirecting } }, ...
也可以指定一个redirectLimit
参数来限制最大的跳转次数,这个值默认是5。如果超过了跳转次数,则会显示一个错误页面。
转场动画
GoRouter
支持为每个 GoRoute
自定义转场动画,这可以通过GoRoute
的构造函数的pageBuilder
参数来完成,下面是示例代码。
GoRoute( path: '/fruit-details', pageBuilder: (context, state) { return CustomTransitionPage( key: state.pageKey, child: FruitDetailsScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { // Change the opacity of the screen using a Curve based on the the animation's value return FadeTransition( opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation), child: child, ); }, ); }, ),
完整的示例代码可以在 GitHub 查看:自定义转场动画源码。
错误处理(404页面)
go_router
为MaterialApp
和CupertinoApp
定义了默认的错误页面,也可以通过 errorBuilder 参数自定义错误页面,代码如下。
GoRouter( /* ... */ errorBuilder: (context, state) => ErrorPage(state.error), );
类型安全路由
除了使用 URL 进行路由导航外,go_router 也通过go_router_builder
插件提供了类型安全路由,这可以通过代码生成来完成。要使用这种方式,需要在pubspec.yaml
增加下面这些依赖。
dev_dependencies: go_router_builder: ^1.0.16 build_runner: ^2.3.3 build_verify: ^3.1.0
定义路由
Then define each route as a class extending GoRouteData and overriding the build method.
class HomeRoute extends GoRouteData { const HomeRoute(); @override Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); }
路由树
路由树基于每个顶层的路由来定义,代码如下。
import 'package:go_router/go_router.dart'; part 'go_router.g.dart'; // name of generated file // Define how your route tree (path and sub-routes) @TypedGoRoute<HomeScreenRoute>( path: '/home', routes: [ // Add sub-routes TypedGoRoute<SongRoute>( path: 'song/:id', ) ] ) // Create your route screen that extends "GoRouteData" and @override "build" // method that return the screen for this route @immutable class HomeScreenRoute extends GoRouteData { @override Widget build(BuildContext context) { return const HomeScreen(); } } @immutable class SongRoute extends GoRouteData { final int id; const SongRoute({required this.id}); @override Widget build(BuildContext context) { return SongScreen(songId: id.toString()); } }
之后可以运行代码生成来构建类型安全路由。
flutter pub global activate build_runner // Optional, if you already have build_runner activated so you can skip this step flutter pub run build_runner build
导航的时候,就不再需要使用 URL的方式了,可以构建一个GoRouteData对象然后调用go()
即可。
TextButton( onPressed: () { const SongRoute(id: 2).go(context); }, child: const Text('Go to song 2'), ),
路由跳转监测
go_router还提供了一个非常有用的特性,那就是路由导航监测NavigatorObserver
。可以通过给GoRouter增加一个NavigatorObserver
对象来监听路由行为,例如 push
、pop
或路由替换(replace
)。这可以通过自定义 NavigatorObserver
的子类完成。
class MyNavigatorObserver extends NavigatorObserver { @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { log('did push route'); } @override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { log('did pop route'); } }
之后,在GoRouter
的 observers
参数中增加自定义的MyNavigatorObserver
即可完成对所有触发路由跳转的行为的监听。
GoRouter( ... observers: [ // Add your navigator observers MyNavigatorObserver(), ], ... )
完整的示例代码见 GitHub: flutter-with-go_router: A simple app to show how to work with go_router。
到此这篇关于Flutter中go_router路由管理的使用指南的文章就介绍到这了,更多相关Flutter go_router内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!