GoRouter 完全指南
go_router 是 Flutter 官方推荐的声明式路由库,基于 Navigator 2.0,支持深链接、Web URL 同步、嵌套路由。 ShellRoute 为子路由提供共享的外壳 Widget(通常是带 BottomNavigationBar 的 Scaffold)。切换 Tab 时子路由各自维护独立的 Navigator,但 Shell Widget 本身不会重建。 StatefulShellRoute 在 ShellRoute 基础上为每个分支维护独立的 Navigator 状态,切换 Tab 时不会销毁页面。 refreshListena
官方文档:https://pub.dev/packages/go_router
适用版本:go_router 14.x(2026-05-07 核实)
go_router 是 Flutter 官方推荐的声明式路由库,基于 Navigator 2.0,支持深链接、Web URL 同步、嵌套路由。
安装
# pubspec.yaml
dependencies:
go_router: ^14.2.0
基础配置
GoRouter 参数
| 参数 | 类型 | 说明 |
|---|---|---|
routes |
List<RouteBase> |
路由列表,必填 |
initialLocation |
String |
初始路径,默认 / |
redirect |
FutureOr<String?> Function(BuildContext, GoRouterState)? |
全局重定向,返回 null 表示不重定向 |
errorBuilder |
Widget Function(BuildContext, GoRouterState)? |
404 页面构建器 |
navigatorKey |
GlobalKey<NavigatorState>? |
自定义 Navigator key |
debugLogDiagnostics |
bool |
打印路由跳转日志,默认 false |
refreshListenable |
Listenable? |
状态变化时触发全局 redirect 重新执行 |
redirect 调用时机 |
- | 每次导航前执行 |
GoRoute 参数
| 参数 | 类型 | 说明 |
|---|---|---|
path |
String |
路径,支持 :param 占位符,必填 |
name |
String? |
命名路由,用于 goNamed |
builder |
Widget Function(BuildContext, GoRouterState)? |
构建 Widget |
pageBuilder |
Page Function(BuildContext, GoRouterState)? |
自定义 Page(控制转场动画) |
redirect |
FutureOr<String?> Function(BuildContext, GoRouterState)? |
路由级重定向 |
routes |
List<RouteBase> |
子路由列表 |
基础示例
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:id',
name: 'detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailScreen(id: id);
},
),
],
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
errorBuilder: (context, state) => const NotFoundScreen(),
);
// 集成到 MaterialApp
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
导航
go / push / pop 区别
| 方法 | 堆栈行为 | 适用场景 |
|---|---|---|
context.go(path) |
替换整个导航堆栈 | Tab 切换、登录后跳首页 |
context.push(path) |
压入堆栈顶部 | 进入详情页,可以返回 |
context.pop() |
弹出当前页面 | 返回上一页 |
context.replace(path) |
替换当前页面 | 不需要返回的跳转 |
context.pushReplacement(path) |
替换堆栈顶部 | 替换当前页,保留下层 |
命名路由导航
// 路径参数 + 查询参数
context.goNamed(
'detail',
pathParameters: {'id': '123'},
queryParameters: {'tab': 'comments'},
);
// 等价于跳转到 /detail/123?tab=comments
路径参数和查询参数读取
GoRoute(
path: '/article/:id',
builder: (context, state) {
// 路径参数
final id = state.pathParameters['id']!;
// 查询参数
final tab = state.uri.queryParameters['tab'] ?? 'content';
return ArticleScreen(id: id, tab: tab);
},
)
返回值传递
// 接收返回值的页面
Future<void> openEditPage() async {
final result = await context.push<bool>('/edit');
if (result == true) {
// 刷新数据
refreshData();
}
}
// 返回值的页面
ElevatedButton(
onPressed: () => context.pop(true), // 传递返回值
child: const Text('保存'),
)
嵌套路由
ShellRoute:共享底部导航栏
ShellRoute 为子路由提供共享的外壳 Widget(通常是带 BottomNavigationBar 的 Scaffold)。切换 Tab 时子路由各自维护独立的 Navigator,但 Shell Widget 本身不会重建。
final router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _calculateIndex(context),
onTap: (index) {
switch (index) {
case 0:
context.go('/home');
case 1:
context.go('/profile');
}
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
int _calculateIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/profile')) return 1;
return 0;
}
}
StatefulShellRoute:保持各 Tab 状态
StatefulShellRoute 在 ShellRoute 基础上为每个分支维护独立的 Navigator 状态,切换 Tab 时不会销毁页面。
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
)
// 使用 navigationShell 切换 Tab
class ScaffoldWithNavBar extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavBar({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
重定向与守卫
全局 redirect(认证守卫)
final router = GoRouter(
redirect: (context, state) {
final isLoggedIn = AuthService.instance.isLoggedIn;
final isLoginPage = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginPage) {
// 未登录跳转到登录页,并记录原始路径
return '/login?redirect=${state.matchedLocation}';
}
if (isLoggedIn && isLoginPage) {
// 已登录不需要再访问登录页
return '/home';
}
return null; // 不重定向
},
routes: [...],
);
路由级 redirect
GoRoute(
path: '/admin',
redirect: (context, state) {
if (!AuthService.instance.isAdmin) {
return '/home';
}
return null;
},
builder: (context, state) => const AdminScreen(),
)
GoRouterState 字段
| 字段 | 类型 | 说明 |
|---|---|---|
uri |
Uri |
完整 URI,包含路径、查询参数 |
matchedLocation |
String |
当前匹配的路径(不含查询参数) |
pathParameters |
Map<String, String> |
路径参数(:id 对应的值) |
extra |
Object? |
通过 extra 参数传递的对象 |
error |
Exception? |
路由错误信息 |
pageKey |
ValueKey<String> |
当前 Page 的唯一 key |
与 Riverpod 集成
监听认证状态自动重定向
// 将 Riverpod Provider 转为 Listenable
class ProviderListenable<T> extends ChangeNotifier {
ProviderListenable(this._ref, this._provider) {
_ref.listen(_provider, (_, __) => notifyListeners());
}
final Ref _ref;
final ProviderListenable<T> _provider;
}
// 推荐:使用 riverpod_router 或自定义 refreshListenable
final authNotifierProvider = ChangeNotifierProvider<AuthNotifier>((ref) {
return AuthNotifier();
});
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
void logout() {
_isLoggedIn = false;
notifyListeners();
}
}
// 在 GoRouter 中使用 refreshListenable
GoRouter buildRouter(WidgetRef ref) {
final authNotifier = ref.watch(authNotifierProvider);
return GoRouter(
refreshListenable: authNotifier,
redirect: (context, state) {
if (!authNotifier.isLoggedIn && state.matchedLocation != '/login') {
return '/login';
}
return null;
},
routes: [...],
);
}
refreshListenable 说明
refreshListenable 接收一个 Listenable,当该对象发出通知时,GoRouter 会重新执行全局 redirect 函数,从而在认证状态变化时自动触发路由更新。
踩坑与注意事项
context.go 与 context.push 的堆栈差异
context.go 会清空现有导航堆栈并跳转,context.push 则保留堆栈。在 Tab 页面间切换时应使用 context.go,否则用户会积累大量历史记录,返回行为异常。
// 错误:Tab 切换用 push 会导致历史堆积
onTap: (index) => context.push('/profile'),
// 正确:Tab 切换用 go
onTap: (index) => context.go('/profile'),
ShellRoute 中的 pop 行为
在 ShellRoute 的子路由中调用 context.pop() 只会弹出该分支内的页面。如果分支内没有更多页面可弹,pop 不会关闭 ShellRoute 本身,这通常是期望的行为,但要注意不要依赖 context.canPop() 来判断是否可以退出 Tab。
路径参数必须与 path 定义完全一致
// path 定义
GoRoute(path: '/user/:userId/post/:postId', ...)
// 读取时参数名必须完全匹配
final userId = state.pathParameters['userId']!;
final postId = state.pathParameters['postId']!;
extra 参数不支持深链接
通过 extra 传递的对象在应用热重载或深链接恢复时会丢失,因为它无法序列化到 URL 中。需要持久化的数据应通过路径参数或查询参数传递。
// 不推荐:深链接场景下 extra 会丢失
context.push('/detail/123', extra: userObject);
// 推荐:通过 ID 传递,在目标页重新获取
context.push('/detail/123');
最佳实践
所有路由集中在一个 GoRouter 实例中声明:避免分散定义路由,集中管理便于全局守卫和深链接配置,通常放在 router.dart 单独文件中,通过 Riverpod/Provider 注入。
用命名路由 context.goNamed 而非路径字符串:路径字符串散落在代码中难以重构,命名路由支持类型安全的 pathParameters:
// 定义
GoRoute(
path: '/user/:id',
name: 'user-detail',
builder: (context, state) => UserDetail(id: state.pathParameters['id']!),
),
// 跳转
context.goNamed('user-detail', pathParameters: {'id': userId});
复杂页面用 ShellRoute 实现嵌套导航:底部导航栏场景用 ShellRoute 包裹,保留各 Tab 的导航栈状态,而非每次切换 Tab 都重建页面。
深链接测试用 adb 命令验证:Android 调试时用 adb shell am start -a android.intent.action.VIEW -d "yourscheme://path" 测试深链接跳转,不要只靠模拟器内点击。
路由守卫统一写在 redirect 回调:登录检查、权限验证放在顶层 GoRouter 的 redirect 中,而非每个页面的 build 方法,保持路由层与 UI 层解耦。
常见陷阱
陷阱:extra 参数在深链接和 Web 刷新后丢失
现象: 通过 context.push('/detail', extra: user) 传递对象,用户刷新页面后 extra 为 null。
原因: extra 存在内存中,不序列化到 URL,深链接和 Web 刷新后无法恢复。
解决: 通过 pathParameters 或 queryParameters 传递 ID,在目标页重新请求数据。
陷阱:context.go 与 context.push 语义混淆
现象: 使用 context.go('/home') 后,返回按钮无法返回上一页。
原因: go 替换整个导航栈,push 在栈顶压入新页面。go 适合 Tab 切换,push 适合页面堆叠。
解决: 导航到新页面用 context.push,重置到根页面(如登出后回首页)用 context.go。
陷阱:redirect 中异步操作导致无限重定向
现象: redirect 回调中读取异步状态时,每次都返回重定向地址,导致页面反复跳转。
原因: redirect 是同步回调,使用 ref.watch 订阅异步 Provider 时 loading 状态触发了不期望的重定向。
解决: 使用 .whenData 或 AsyncValue.guard,loading 状态时返回 null(不重定向):
redirect: (context, state) {
final authState = ref.read(authProvider);
if (authState.isLoading) return null; // 加载中不做任何重定向
if (!authState.isAuthenticated) return '/login';
return null;
},