关于 navigation-and-routing
面向 Flutter 应用开发者的导航与路由技能,规范使用 go_router 构建单一、可深链访问的路由结构,并涵盖守卫、标签页导航和返回行为。
主要功能
- 01单一 GoRouter 配置
- 02基于 URL 的深链路由
- 03纯函数式重定向守卫
- 04保留状态的标签页分支
- 05转场与返回行为规范
- 06路由检查脚本
适用场景
- 01统一应用的路由配置
- 02确保详情页可由深链重建
- 03实现认证与引导流程守卫
- 04检查提交前的路由规则
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill navigation-and-routing安装后在 Claude Code 中输入/navigation-and-routing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
examples/app_router.dartauth_redirect.dartshell_scaffold.dartreferences/go-router-config.mdguards-and-redirects.mdshells-and-deep-links.mdscripts/check_routing.sh详细介绍
导航与路由
此技能负责使用 go_router 管理应用导航。应用中只有一个 GoRouter,定义在 lib/routing/ 中,每个屏幕都可通过 URL 访问。导航是一种数据结构(路由 + 纯重定向函数),而不是一堆命令式的 Navigator.push 调用。
请阅读与当前任务相关的参考文档:
references/go-router-config.md— 唯一路由器、context.go与context.push的区别、路径参数与state.extra、类型化路由辅助工具、errorBuilder/404。references/guards-and-redirects.md— 纯redirect函数、RiverpodrefreshListenable、身份验证与引导流程拦截、避免重定向循环。references/shells-and-deep-links.md— 底部导航栏/导航栏外壳的StatefulShellRoute.indexedStack、CustomTransitionPage、PopScope,以及通知负载到位置的映射。
提交 PR 前运行 scripts/check_routing.sh。
不可妥协的规则
- 应用中只能有一个
GoRouter,在lib/routing/中构建,并通过app.dart中的MaterialApp.router挂载一次。 多个路由器会分割历史记录、深度链接和返回按钮行为。路由器应在 provider 中创建,以便拦截器可以监听应用状态。 - 可通过深度链接访问的标识必须放在路径参数中,绝不能放在
state.extra中。extra是一个实时 Dart 对象:从深度链接冷启动,以及进程终止后恢复时,它的值都是null。需要通过 id 重建的屏幕必须从state.pathParameters读取 id,这样只凭 URL 就能完整还原屏幕。 state.extra只能用于可选的非标识优化(预先获取对象,避免重新加载时闪烁)。即使extra为null,屏幕也必须能够正常工作——通过 id 重新获取数据。- 使用
context.go替换导航栈(声明式目标位置、标签页、登录后的主页);使用context.push压入一个预期可以返回的屏幕(详情页、模态流程)。 混用不当会破坏返回按钮行为。必须明确每个调用点应该使用哪一种。 - 拦截器必须是纯
redirect函数。redirect根据GoRouterState和应用状态快照返回新的位置String?(或返回null以允许导航)。redirect中不得进行 I/O、调用导航或产生副作用——它会在每次导航时运行,也可能重复运行。 - 响应式拦截器应使用
refreshListenable,而不是轮询。 将 Riverpod 身份验证/引导流程 provider 连接到Listenable;每当它触发,路由器都会重新评估redirect。参见state-management-riverpod。 - 重定向不得形成循环。 始终允许拦截器重定向到的目标位置(例如,绝不能将
/sign-in重定向回/sign-in)。重定向前检查当前位置,以避免A→B→A循环。 - 标签页/分支外壳应使用
StatefulShellRoute.indexedStack。 它会在切换标签页时保留每个分支的导航栈和状态;手动配置IndexedStack的普通ShellRoute无法在路由器重建时同样可靠地保留状态。使用navigationShell.goBranch(index)切换分支。 - 自定义过渡应通过
CustomTransitionPage实现,并遵循减少动态效果设置。 当平台要求减少动态效果时,应改为无动画或淡入淡出。通过MediaQuery读取设置,并使用设计系统解析动态效果——参见accessibility-as-code和design-system-structure。 - 使用
PopScope拦截返回操作或处理未保存的更改,不要使用WillPopScope(已移除)。设置canPop: false,并在onPopInvokedWithResult(bool didPop, T? result)中处理;只有在用户确认后才能离开。 - 提供
errorBuilder和真实的 404/错误路由。 未匹配的深度链接必须进入设计完善的错误屏幕,绝不能显示红色错误框。 - 导航前,不要在
await期间持有BuildContext。 在await之前捕获GoRouter.of(context)(或路由器),或在之后通过context.mounted进行检查。参见async-safety。 - 功能模块不得自行构建路由器。
scaffold-feature-module会将功能的GoRoute注册到此路由器的路由列表中;功能模块绝不能实例化GoRouter。
唯一的路由器
app.dart 从 provider 读取路由器,并将其传给 MaterialApp.router。路由器本身位于 routing/ 中,也是唯一构造 GoRouter(...) 的地方。
// routing/app_router.dart
final routerProvider = Provider<GoRouter>((ref) {
// Bridge Riverpod auth state to a Listenable the router can watch.
final refresh = ValueNotifier<int>(0);
ref.onDispose(refresh.dispose);
ref.listen(authNotifierProvider, (_, __) => refresh.value++);
return GoRouter(
initialLocation: Routes.home,
refreshListenable: refresh,
redirect: (context, state) => appRedirect(ref.read(authNotifierProvider), state),
errorBuilder: (context, state) => ErrorScreen(error: state.error),
routes: $appRoutes, // assembled from feature route lists
);
});
// app.dart — the only MaterialApp.router
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
theme: lightTheme,
darkTheme: darkTheme,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
}
}
身份信息放在路径中,而不是 extra 中
// GOOD: id is in the URL — a cold-start deep link to /items/42 fully rebuilds.
GoRoute(
path: '/items/:id',
builder: (context, state) {
final id = state.pathParameters['id']!; // always present
// extra is an OPTIONAL fast-path; screen must work when it is null.
final preloaded = state.extra as Item?;
return ItemScreen(itemId: id, preloaded: preloaded);
},
),
// BAD: identity smuggled through extra — null on cold start / after process death.
context.push('/item', extra: item); // no id in the URL => not deep-linkable
类型化路由辅助工具(常量,而非字符串拼凑)
优先使用小型常量类或构建器类,让调用处不必手动拼接路径。go_router_builder 代码生成是可选升级方案,而非默认做法。
// routing/routes.dart
abstract final class Routes {
static const home = '/';
static const items = '/items';
static String item(String id) => '/items/$id';
static const signIn = '/sign-in';
}
// call site
context.push(Routes.item(item.id));
go 与 push
context.go(Routes.home); // replace whole stack: post-login, tab roots
context.push(Routes.item(id)); // stack a detail you'll pop back from
context.pop(result); // return up, optionally with a result
反模式
- 创建两个
GoRouter实例,或在屏幕中嵌套Navigator/MaterialApp来实现“子导航”。请使用嵌套路由 /StatefulShellRoute。 - 通过
state.extra传递领域 ID,并在build中读取extra!—— 冷启动时会崩溃。 - 在
redirect中执行 I/O、对异步工作调用ref.read,或调用context.go。重定向应保持纯粹并返回一个位置。 redirect可能无限跳转,因为它也会重定向到自身的目标。- 将
Navigator.pushNamed('/x')字符串路由与 go_router 混用 —— 只能使用一种导航系统。 - 使用已移除的
WillPopScope,而非PopScope。 - 等待异步操作后,未经
mounted检查就使用同一个context导航。 - 在功能包 / 文件夹中自行构造
GoRouter。
完成标准
lib/routing/中只有一个GoRouter,app.dart中只有一个MaterialApp.router。- 每个屏幕都可通过 URL 访问;每个带 ID 的屏幕都从
state.pathParameters读取 ID。 state.extra只能作为可选优化;即使extra == null,所有这类屏幕也都能正确渲染。- 守卫是纯粹的
redirect函数,并带有refreshListenable;不存在重定向循环。 - 标签页外壳使用
StatefulShellRoute.indexedStack;分支状态在切换标签页时得以保留。 - 转场尊重减少动态效果设置;提供
errorBuilder和 404 路由。 PopScope用于保护未保存的更改;导航时不会在await期间使用BuildContext。scripts/check_routing.sh通过。
相关技能
app-startup-and-bootstrap—— 负责main()/bootstrap()的顺序,以及MaterialApp.router的挂载位置。state-management-riverpod——refreshListenable所桥接的身份验证 / 入门引导 provider。scaffold-feature-module—— 将功能GoRoute注册到此路由器中。adaptive-layout—— 根据宽度为外壳选择NavigationRail或BottomNavigationBar。accessibility-as-code—— 导航的减少动态效果标志和语义信息。design-system-structure—— 用于转场的减少动态效果 token /resolveMotion辅助工具。async-safety—— 等待导航时关于BuildContext/mounted的规则。local-notifications-scheduler—— 通知载荷及其生成位置的纯映射器。
参考资料
- go_router 包:https://pub.dev/packages/go_router
- go_router API 文档:https://pub.dev/documentation/go_router/latest/
- Flutter 导航与路由:https://docs.flutter.dev/ui/navigation
- 深度链接:https://docs.flutter.dev/ui/navigation/deep-linking
- PopScope API:https://api.flutter.dev/flutter/widgets/PopScope-class.html
评论
支持 Markdown