navigation-and-routing について
go_router を使う Flutter 開発者向けのナビゲーション設計スキルです。単一の URL ベースのルーターを軸に、リダイレクト、タブシェル、ディープリンク、戻る操作を扱います。
主な機能
- 01単一の GoRouter 設定
- 02URL ベースのディープリンク
- 03純粋なリダイレクトガード
- 04状態を保つタブ分岐
- 05画面遷移と戻る操作の規則
- 06ルーティング検査スクリプト
活用シーン
- 01アプリのルート設定を統一
- 02ディープリンクから詳細画面を復元
- 03認証やオンボーディングを制御
- 04PR 前にルーティング規則を確認
インストール
コマンドをコピーしてターミナルで実行します。
$
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 は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 を実行してください。
必ず守るルール
GoRouterは1つだけとし、lib/routing/で構築して、app.dartのMaterialApp.routerから1回だけ接続します。 複数のルーターを使うと、履歴、ディープリンク、戻るボタンの動作が分断されます。ガードがアプリの状態を監視できるよう、ルーターはプロバイダーを通して作成します。- ディープリンクで必要な識別情報は、必ずパスパラメーターに置き、
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 の認証/オンボーディングプロバイダーを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を参照してください。 - 戻る操作や未保存の変更は、削除済みの
WillPopScopeではなくPopScopeで処理します。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 はプロバイダーからルーターを読み取り、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— 純粋なマッパーが遷移先を生成する通知ペイロードを扱います。
参考資料
- 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 対応