在上一篇关于导航的文章中,我们学习了 Navigator 1.0,也就是大家所熟知的 push 和 pop 方法。对于简单的线性页面流,它工作得非常好。但随着应用变得越来越复杂,Navigator 1.0 的局限性也逐渐显现。
想象以下场景:
- 深度链接 (Deep Linking) :用户点击一个
URL(myapp://details/123),需要直接打开应用的特定页面。 - Web 应用支持 :在
Flutter Web中,你需要同步浏览器的地址栏和历史记录(前进/后退按钮)。 - 复杂嵌套导航:应用拥有底部导航栏,每个标签页都维护着自己的导航堆栈。
用 Navigator 1.0 (命令式路由) 来处理这些需求,会变得异常棘手和混乱。
为了解决这些问题,Flutter 团队引入了 Navigator 2.0 (声明式路由)。但不得不承认,Navigator 2.0 的原生 API(如 Router, RouterDelegate, RouteInformationParser)非常复杂,学习曲线陡峭,劝退了大量开发者。
幸运的是,我们不必直接和这些复杂的 API 打交道。Flutter 官方团队推出了 GoRouter,一个构建在 Navigator 2.0 之上的、强大且简单易用的路由库。它完美地简化了声明式路由,并成为当前 Flutter 路由管理的首选方案。
这篇文章,就是你掌握 GoRouter 的完全指南。
1. 为什么选择 GoRouter?
- 基于 URL :以我们熟悉的
URL字符串来定义和导航页面,直观且易于管理。 - 声明式 :你只需要定义好路由表,
GoRouter会自动处理底层的Navigator 2.0逻辑。 - 完美支持 Web:自动处理浏览器地址栏同步和历史记录。
- 类型安全:新版本支持类型安全地传递参数,减少运行时错误。
- 轻松处理复杂场景:无论是参数传递、嵌套路由、还是路由重定向(如登录拦截),都提供了优雅的解决方案。
2. GoRouter 实战演练
让我们从一个简单的项目开始,一步步探索 GoRouter 的核心功能。
第一步:添加依赖
首先,在你的 pubspec.yaml 文件中添加 go_router 依赖。
yaml
dependencies:
flutter:
sdk: flutter
go_router: ^14.1.0 # 建议使用最新版本
然后运行 flutter pub get 安装。
第二步:创建基础路由配置
创建一个单独的文件来管理路由,例如 lib/router/app_router.dart。
dart
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
// 导入你的页面 Widget
import '../screens/home_screen.dart';
import '../screens/details_screen.dart';
import '../screens/not_found_screen.dart';
final GoRouter router = GoRouter(
// 404 页面
errorBuilder: (context, state) => const NotFoundScreen(),
// 路由配置
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/details',
builder: (context, state) => const DetailsScreen(),
),
],
);
GoRouter(...):这是路由配置的入口。errorBuilder:当用户访问一个不存在的路径时,会显示这个页面。routes: 一个GoRoute列表,定义了应用的每个页面。path: 页面的访问路径 (URL)。builder: 一个函数,返回对应路径的Widget。
第三步:集成到 MaterialApp
现在,回到你的 main.dart,使用 MaterialApp.router 来集成 GoRouter。
dart
import 'package:flutter/material.dart';
import 'router/app_router.dart'; // 导入你的路由配置
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// 使用 MaterialApp.router 构造函数
return MaterialApp.router(
title: 'GoRouter Demo',
// 将我们创建的 router 对象配置给 routerConfig
routerConfig: router,
);
}
}
至此,你的应用已经由 GoRouter 全权接管路由了!
第四步:页面跳转 (Navigation)
GoRouter 提供了一组 BuildContext 的扩展方法来进行导航,非常方便。
在你的 HomeScreen 中:
dart
// home_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 跳转到 /details 页面
context.go('/details');
},
child: const Text('Go to Details'),
),
),
);
}
}
context.go(path):跳转到新页面,它会替换 当前的路由堆栈。这更适用于Web行为或主标签页切换。context.push(path):将新页面推入 到当前的路由堆栈之上,这和Navigator.push行为一致,通常用于从列表页进入详情页。
在详情页,我们可以使用 context.pop() 返回:
dart
// details_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页
context.pop();
},
child: const Text('Go Back'),
),
),
);
}
}
第五步:传递参数
这是 GoRouter 的强项。我们有三种常见的传参方式:
1. 路径参数 (Path Parameters)
这适用于传递简单的 ID 或标识符。
修改路由配置,在 path 中使用冒号 (:) 定义参数:
dart
// app_router.dart
GoRoute(
// :id 是一个占位符,匹配 /details/任意字符串
path: '/details/:id',
builder: (context, state) {
// 通过 state.pathParameters 获取路径参数
final String id = state.pathParameters['id'] ?? '0';
return DetailsScreen(id: id);
},
),
修改 DetailsScreen 以接收参数:
dart
// details_screen.dart
class DetailsScreen extends StatelessWidget {
final String id;
const DetailsScreen({super.key, required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details Screen')),
body: Center(
child: Text('You are viewing details for item: $id'),
),
// ...
);
}
}
跳转时,直接在路径中提供参数值:
dart
// home_screen.dart
context.push('/details/123'); // 跳转到详情页,并传递 id '123'
2. 查询参数 (Query Parameters)
适用于传递过滤、搜索等非必需参数。
路由配置不需要改变。跳转时,像写 URL 一样附加参数:
dart
context.push('/details/123?source=homepage&sort=asc');
在 DetailsScreen 中,通过 state.uri.queryParameters 获取:
dart
// details_screen.dart builder function
builder: (context, state) {
final String id = state.pathParameters['id'] ?? '0';
final String? source = state.uri.queryParameters['source']; // "homepage"
return DetailsScreen(id: id, source: source);
}
3. extra 参数
当你想传递一个复杂的自定义对象(如 Model 实例)时,extra 参数是最好的选择。
跳转时,在 push 或 go 方法中传入 extra:
dart
// 假设你有一个 Product 类
final product = Product(id: '123', name: 'Flutter Book');
context.push('/details/123', extra: product);
在 DetailsScreen 的 builder 中接收它:
dart
builder: (context, state) {
final String id = state.pathParameters['id']!;
// state.extra 是 Object? 类型,需要进行类型转换
final Product? product = state.extra as Product?;
return DetailsScreen(id: id, product: product);
}
第六步:嵌套路由 (Nested Navigation)
GoRouter 让实现带底部导航栏的复杂布局变得非常简单。我们将使用 StatefulShellRoute.indexedStack。
想象一个应用有三个主页面:Home, Feed, Settings。
dart
// app_router.dart
// 1. 定义一个 shell widget,它包含 BottomNavigationBar 和用于显示子页面的 body
import '../screens/scaffold_with_nav_bar.dart';
final GoRouter router = GoRouter(
initialLocation: '/home', // 初始页面
routes: [
// 2. 使用 StatefulShellRoute.indexedStack 定义嵌套路由
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
// 返回你的 Shell UI (例如,一个带 BottomNavigationBar的 Scaffold)
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
// 第一个分支 (tab)
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
],
),
// 第二个分支
StatefulShellBranch(
routes: [
GoRoute(
path: '/feed',
builder: (context, state) => const FeedScreen(),
routes: [
// Feed 页面下的子页面
GoRoute(
path: 'details/:id', // 路径会自动拼接为 /feed/details/:id
builder: (context, state) => DetailsScreen(id: state.pathParameters['id']!),
),
]
),
],
),
// 第三个分支
StatefulShellBranch(
routes: [
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
],
),
],
),
],
);
你的 ScaffoldWithNavBar 会接收一个 navigationShell 对象,用它来构建 BottomNavigationBar 并显示当前分支的页面。 StatefulShellRoute 的美妙之处在于,当你切换标签页时,每个分支的导航状态都会被完整保留!
第七步:重定向 (Redirect) - 登录验证
GoRouter 的 redirect 功能是实现路由守卫(例如登录拦截)的利器。
dart
// app_router.dart
final GoRouter router = GoRouter(
redirect: (BuildContext context, GoRouterState state) {
// 模拟一个登录状态
final bool loggedIn = false; // 在真实应用中,你会从状态管理中获取
// 检查用户是否在访问登录页
final bool isLoggingIn = state.matchedLocation == '/login';
// 如果用户未登录且不在登录页,重定向到登录页
if (!loggedIn && !isLoggingIn) {
return '/login';
}
// 如果用户已登录且在访问登录页,重定向到首页
if (loggedIn && isLoggingIn) {
return '/';
}
// 其他情况,不重定向
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
);
这个 redirect 函数会在每次导航发生前被调用。你可以根据应用状态(如登录状态、用户权限等)返回一个新的路径来中断并重定向导航,或者返回 null 表示允许本次导航。
总结
Navigator 2.0 的声明式思想为 Flutter 带来了更强大、更灵活的路由系统,而 GoRouter 则是我们驾驭这套系统的最佳工具。
通过本教程,我们学习了 GoRouter 的核心用法:
- 基础配置 :通过
GoRouter和GoRoute定义路由表。 - 页面导航 :使用
context.go,context.push,context.pop。 - 参数传递 :掌握了路径参数、查询参数和
extra对象传参。 - 嵌套路由 :使用
StatefulShellRoute轻松实现复杂的UI布局。 - 路由重定向 :通过
redirect实现登录拦截等路由守卫。
掌握 GoRouter,你将能够自信地构建任何复杂的 Flutter 应用,并轻松应对 Web 和深度链接带来的挑战。