Flutter go_router 路由管理全面指南
本篇我们介绍一个非常受欢迎的路由管理第三方插件: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)
来完成切换动作。
路由守卫(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,
);
},
);
},
),
错误处理(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(),
],
...
)