mcpField
N
navigation-and-routing

navigation-and-routing

github.com/zakariaf开发63164 次安装5 周前更新
代码仓库

关于 navigation-and-routing

面向 Flutter 应用开发者的导航与路由技能,规范使用 go_router 构建单一、可深链访问的路由结构,并涵盖守卫、标签页导航和返回行为。

主要功能

  1. 01单一 GoRouter 配置
  2. 02基于 URL 的深链路由
  3. 03纯函数式重定向守卫
  4. 04保留状态的标签页分支
  5. 05转场与返回行为规范
  6. 06路由检查脚本

适用场景

  1. 01统一应用的路由配置
  2. 02确保详情页可由深链重建
  3. 03实现认证与引导流程守卫
  4. 04检查提交前的路由规则

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zakariaf/flutter-skills --skill navigation-and-routing

安装后在 Claude Code 中输入/navigation-and-routing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

examples/
app_router.dart
auth_redirect.dart
shell_scaffold.dart
references/
go-router-config.md
guards-and-redirects.md
shells-and-deep-links.md
scripts/
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 函数、Riverpod refreshListenable、身份验证与引导流程拦截、避免重定向循环。
  • references/shells-and-deep-links.md — 底部导航栏/导航栏外壳的 StatefulShellRoute.indexedStack、CustomTransitionPage、PopScope,以及通知负载到位置的映射。

提交 PR 前运行 scripts/check_routing.sh。

不可妥协的规则

  1. 应用中只能有一个 GoRouter,在 lib/routing/ 中构建,并通过 app.dart 中的 MaterialApp.router 挂载一次。 多个路由器会分割历史记录、深度链接和返回按钮行为。路由器应在 provider 中创建,以便拦截器可以监听应用状态。
  2. 可通过深度链接访问的标识必须放在路径参数中,绝不能放在 state.extra 中。 extra 是一个实时 Dart 对象:从深度链接冷启动,以及进程终止后恢复时,它的值都是 null。需要通过 id 重建的屏幕必须从 state.pathParameters 读取 id,这样只凭 URL 就能完整还原屏幕。
  3. state.extra 只能用于可选的非标识优化(预先获取对象,避免重新加载时闪烁)。即使 extra 为 null,屏幕也必须能够正常工作——通过 id 重新获取数据。
  4. 使用 context.go 替换导航栈(声明式目标位置、标签页、登录后的主页);使用 context.push 压入一个预期可以返回的屏幕(详情页、模态流程)。 混用不当会破坏返回按钮行为。必须明确每个调用点应该使用哪一种。
  5. 拦截器必须是纯 redirect 函数。 redirect 根据 GoRouterState 和应用状态快照返回新的位置 String?(或返回 null 以允许导航)。redirect 中不得进行 I/O、调用导航或产生副作用——它会在每次导航时运行,也可能重复运行。
  6. 响应式拦截器应使用 refreshListenable,而不是轮询。 将 Riverpod 身份验证/引导流程 provider 连接到 Listenable;每当它触发,路由器都会重新评估 redirect。参见 state-management-riverpod。
  7. 重定向不得形成循环。 始终允许拦截器重定向到的目标位置(例如,绝不能将 /sign-in 重定向回 /sign-in)。重定向前检查当前位置,以避免 A→B→A 循环。
  8. 标签页/分支外壳应使用 StatefulShellRoute.indexedStack。 它会在切换标签页时保留每个分支的导航栈和状态;手动配置 IndexedStack 的普通 ShellRoute 无法在路由器重建时同样可靠地保留状态。使用 navigationShell.goBranch(index) 切换分支。
  9. 自定义过渡应通过 CustomTransitionPage 实现,并遵循减少动态效果设置。 当平台要求减少动态效果时,应改为无动画或淡入淡出。通过 MediaQuery 读取设置,并使用设计系统解析动态效果——参见 accessibility-as-code 和 design-system-structure。
  10. 使用 PopScope 拦截返回操作或处理未保存的更改,不要使用 WillPopScope(已移除)。设置 canPop: false,并在 onPopInvokedWithResult(bool didPop, T? result) 中处理;只有在用户确认后才能离开。
  11. 提供 errorBuilder 和真实的 404/错误路由。 未匹配的深度链接必须进入设计完善的错误屏幕,绝不能显示红色错误框。
  12. 导航前,不要在 await 期间持有 BuildContext。 在 await 之前捕获 GoRouter.of(context)(或路由器),或在之后通过 context.mounted 进行检查。参见 async-safety。
  13. 功能模块不得自行构建路由器。 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 —— 通知载荷及其生成位置的纯映射器。

参考资料

评论

支持 Markdown