mcpField
N
navigation-and-routing

navigation-and-routing

github.com/zakariaf開発63164 回インストール5 週間前に更新
リポジトリ

navigation-and-routing について

go_router を使う Flutter 開発者向けのナビゲーション設計スキルです。単一の URL ベースのルーターを軸に、リダイレクト、タブシェル、ディープリンク、戻る操作を扱います。

主な機能

  1. 01単一の GoRouter 設定
  2. 02URL ベースのディープリンク
  3. 03純粋なリダイレクトガード
  4. 04状態を保つタブ分岐
  5. 05画面遷移と戻る操作の規則
  6. 06ルーティング検査スクリプト

活用シーン

  1. 01アプリのルート設定を統一
  2. 02ディープリンクから詳細画面を復元
  3. 03認証やオンボーディングを制御
  4. 04PR 前にルーティング規則を確認

インストール

コマンドをコピーしてターミナルで実行します。

$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 は1つだけ存在し、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 は1つだけとし、lib/routing/ で構築して、app.dart の MaterialApp.router から1回だけ接続します。 複数のルーターを使うと、履歴、ディープリンク、戻るボタンの動作が分断されます。ガードがアプリの状態を監視できるよう、ルーターはプロバイダーを通して作成します。
  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 の認証/オンボーディングプロバイダーを 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. 戻る操作や未保存の変更は、削除済みの WillPopScope ではなく PopScope で処理します。 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 はプロバイダーからルーターを読み取り、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 ではなくパスに ID を含める

// 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 を 2 つ作成すること、または「サブナビゲーション」のために画面内にネストした Navigator/MaterialApp を配置すること。ネストしたルート/StatefulShellRoute を使ってください。
  • ドメイン ID を state.extra 経由で渡し、build 内で extra! を読み取ること。コールドスタート時にクラッシュします。
  • redirect 内での I/O、非同期処理に対する ref.read、または context.go の呼び出し。リダイレクトは純粋関数とし、遷移先を返します。
  • リダイレクト先自体もリダイレクトするため、無限にループする可能性のある redirect。
  • go_router と Navigator.pushNamed('/x') の文字列ルートを混在させること。ナビゲーションシステムは 1 つだけ使ってください。
  • 削除済みの WillPopScope を使い、PopScope を使わないこと。
  • mounted を確認せずに、await 後に同じ context を使ってナビゲーションすること。
  • フィーチャーパッケージ/フォルダー内で独自の GoRouter を構築すること。

完了の定義

  • lib/routing/ に GoRouter が 1 つ、app.dart に MaterialApp.router が 1 つあること。
  • すべての画面に URL からアクセスでき、ID を持つすべての画面が state.pathParameters から ID を読み取ること。
  • state.extra は任意の最適化としてのみ使用し、それを使うすべての画面が extra == null でも正しく表示されること。
  • ガードは refreshListenable を備えた純粋な redirect 関数であり、リダイレクトループがないこと。
  • タブシェルでは StatefulShellRoute.indexedStack を使い、タブ切り替え後もブランチの状態が維持されること。
  • トランジションが視覚効果の低減設定に対応し、errorBuilder と 404 ルートが用意されていること。
  • 未保存の変更を PopScope で保護し、ナビゲーション時に await をまたいで BuildContext を使用しないこと。
  • scripts/check_routing.sh が成功すること。

関連スキル

  • app-startup-and-bootstrap — main()/bootstrap() の順序と、MaterialApp.router を配置する場所を管理します。
  • state-management-riverpod — refreshListenable が連携する認証/オンボーディングのプロバイダーです。
  • scaffold-feature-module — フィーチャーの GoRoute をこのルーターに登録します。
  • adaptive-layout — シェルの幅に応じて NavigationRail と BottomNavigationBar のどちらを使うか選択します。
  • accessibility-as-code — ナビゲーションの視覚効果低減フラグとセマンティクスを扱います。
  • design-system-structure — トランジション用の視覚効果低減トークン/resolveMotion ヘルパーを扱います。
  • async-safety — await 後のナビゲーションにおける BuildContext/mounted のルールを扱います。
  • local-notifications-scheduler — 純粋なマッパーが遷移先を生成する通知ペイロードを扱います。

参考資料

コメント