Flutter Navigation 2.0とGoRouter 2026年完全ガイド:ディープリンクから面接対策まで

GoRouter 17.5を使用したFlutter Navigation 2.0の実装方法を解説します。ディープリンク設定、ShellRoute、StatefulShellRoute、認証ガード、型安全ルーティングまで、本番アプリに必要な知識を網羅します。

Flutter Navigation 2.0 と GoRouter によるディープリンク実装図

Flutter のナビゲーションは、Navigation 2.0 の導入以来、大きく進化しました。Flutter チーム公式パッケージである GoRouter 17.5 は、Router API の複雑さを抽象化し、宣言的ルーティング、自動ディープリンク、ネストされたナビゲーションを標準でサポートします。

GoRouterは機能完備

Flutter チームは GoRouter を2026年時点で機能完備と見なしています。パスパラメータ、クエリパラメータ、リダイレクト、永続的UI用の ShellRoute、タブ状態保持用の StatefulShellRoute をサポートします。生の Navigator 2.0 を手書きで実装することは、本番環境ではほとんどありません。

Navigator 2.0 は、宣言的で URL 駆動型のアプローチを Flutter ナビゲーションに導入しました。命令型の pushpop 呼び出しの代わりに、アプリの状態がナビゲーションスタックに表示される内容を決定します。このアーキテクチャは3つのコアクラスに依存しています。

  • Router: ナビゲーションを調整するトップレベルウィジェット
  • RouteInformationParser: URL をアプリ状態に変換
  • RouterDelegate: その状態に基づいてウィジェットツリーを構築

OS からディープリンクが到着するか、Web ブラウザで URL が変更されると、フレームワークが状態を更新し、UI が反応します。問題点:カスタムの RouterDelegateRouteInformationParser を手書きで作成するには、大量のボイラープレートが必要です。

raw_navigator2_example.dartdart
// Raw Navigator 2.0 approach - verbose and rarely used directly
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
  
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  AppRoutePath? _currentPath;

  
  AppRoutePath? get currentConfiguration => _currentPath;

  
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        const MaterialPage(child: HomeScreen()),
        if (_currentPath?.isProductPage == true)
          MaterialPage(child: ProductScreen(id: _currentPath!.productId!)),
      ],
      onDidRemovePage: (page) {
        // Handle page removal
      },
    );
  }

  
  Future<void> setNewRoutePath(AppRoutePath path) async {
    _currentPath = path;
    notifyListeners();
  }
}

この冗長性が GoRouter が存在する理由です。Navigator 2.0 をラップし、ボイラープレートを処理します。

GoRouter 17.5 のセットアップ

GoRouter 17.5 では、ルートメタデータサポートとパスパラメータの正規表現制約が導入されました。最小 SDK 要件は Flutter 3.32 と Dart 3.8 です。

router_config.dartdart
import 'package:go_router/go_router.dart';

final goRouter = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true, // Useful during development
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'products/:id', // Path parameter
          name: 'product',
          builder: (context, state) {
            final productId = state.pathParameters['id']!;
            return ProductScreen(productId: productId);
          },
        ),
        GoRoute(
          path: 'search',
          name: 'search',
          builder: (context, state) {
            // Query parameters: /search?q=flutter&category=books
            final query = state.uri.queryParameters['q'] ?? '';
            final category = state.uri.queryParameters['category'];
            return SearchScreen(query: query, category: category);
          },
        ),
      ],
    ),
  ],
);

ルーターは MaterialApp.router と統合されます。

main.dartdart
import 'package:flutter/material.dart';
import 'router_config.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: goRouter,
      title: 'GoRouter Demo',
    );
  }
}

Android と iOS のディープリンク設定

ディープリンクにより、外部 URL がアプリ内の特定の画面を開くことができます。プラットフォーム設定が完了すれば、GoRouter が自動的にルーティングを処理します。

Android では AndroidManifest.xmlintent-filter を設定し、ドメインに Digital Asset Links ファイルをホストする必要があります。

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
      android:scheme="https"
      android:host="example.com"
      android:pathPrefix="/products" />
  </intent-filter>
</activity>

assetlinks.json ファイルは https://example.com/.well-known/assetlinks.json で配信される必要があります。

iOS では、Xcode で Associated Domains capability を有効にし、apple-app-site-association ファイルをホストする必要があります。

https://example.com/.well-known/apple-app-site-associationjson
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.app",
        "paths": ["/products/*", "/search"]
      }
    ]
  }
}

GoRouter 17.5 では、空のパスを持つ Android のコールドスタートディープリンクがスキームと authority を失う重大なバグが修正されました。これにより、アプリが実行されていない場合でもディープリンクが確実に動作します。

ディープリンクのテスト

Android のディープリンクは adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123" でテストできます。iOS の場合は xcrun simctl openurl booted "https://example.com/products/123" を使用します。

永続的ナビゲーション UI のための ShellRoute

ShellRoute は、BottomNavigationBarDrawer のような永続的な UI 要素で子ルートをラップします。シェルは子ルート間のナビゲーション中も表示されたままになります。

shell_router_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    ShellRoute(
      builder: (context, state, child) {
        return ScaffoldWithNavBar(child: child);
      },
      routes: [
        GoRoute(
          path: '/home',
          name: 'home',
          builder: (context, state) => const HomeTab(),
        ),
        GoRoute(
          path: '/explore',
          name: 'explore',
          builder: (context, state) => const ExploreTab(),
        ),
        GoRoute(
          path: '/profile',
          name: 'profile',
          builder: (context, state) => const ProfileTab(),
        ),
      ],
    ),
  ],
);

// scaffold_with_nav_bar.dart
class ScaffoldWithNavBar extends StatelessWidget {
  final Widget child;
  const ScaffoldWithNavBar({super.key, required this.child});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _calculateSelectedIndex(context),
        onTap: (index) => _onItemTapped(index, context),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }

  int _calculateSelectedIndex(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    if (location.startsWith('/explore')) return 1;
    if (location.startsWith('/profile')) return 2;
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0: context.go('/home');
      case 1: context.go('/explore');
      case 2: context.go('/profile');
    }
  }
}

GoRouter 17.5 では、iOS のバックジェスチャーがアクティブなサブルートではなく ShellRoute 全体をポップしてしまう問題が修正されました。

Flutterの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

タブ状態を保持する StatefulShellRoute

ユーザーがタブを切り替えると、ShellRoute は子を再構築します。StatefulShellRoute は各ブランチのナビゲーションスタックを保持します。

stateful_shell_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeTab(),
              routes: [
                GoRoute(
                  path: 'details/:id',
                  builder: (context, state) => DetailsScreen(
                    id: state.pathParameters['id']!,
                  ),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/explore',
              builder: (context, state) => const ExploreTab(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              builder: (context, state) => const ProfileTab(),
            ),
          ],
        ),
      ],
    ),
  ],
);

この設定により、ユーザーが /home/details/42 に移動し、Explore タブに切り替え、その後 Home に戻ると、details 画面が表示されたままになります。

リダイレクトによるルートガード

リダイレクトは認証チェックと条件付きナビゲーションを処理します。redirect コールバックは各ナビゲーションの前に実行されます。

authenticated_router.dartdart
final goRouter = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final isLoggingIn = state.matchedLocation == '/login';

    // Redirect to login if not authenticated
    if (!isLoggedIn && !isLoggingIn) {
      return '/login?redirect=${state.uri}';
    }

    // Redirect away from login if already authenticated
    if (isLoggedIn && isLoggingIn) {
      final redirect = state.uri.queryParameters['redirect'];
      return redirect ?? '/';
    }

    return null; // No redirect
  },
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/admin',
      redirect: (context, state) {
        // Route-level redirect for admin-only access
        if (!AuthService.instance.isAdmin) {
          return '/';
        }
        return null;
      },
      builder: (context, state) => const AdminScreen(),
    ),
  ],
);

GoRouter 17.5 では、リダイレクトコールバック内で context.namedLocation() などのコンテキスト拡張メソッドを使用できるようになりました。

Flutter ナビゲーション面接でよく聞かれる質問

面接官は、ナビゲーションがアーキテクチャ、状態管理、プラットフォーム統合に関わるため、この知識を頻繁にテストします。シニア候補者を見分ける質問を紹介します。

Q: Navigator 2.0 は Navigator 1.0 では解決できないどのような問題を解決しますか?

Navigator 1.0 は命令型ナビゲーションを使用します:Navigator.push()Navigator.pop()。アプリはナビゲーション状態を URL として表現できないため、ディープリンクと Web サポートが困難です。Navigator 2.0 は宣言的です:アプリ状態がナビゲーションスタックを決定し、URL はファーストクラスの市民であり、ディープリンクは自動的に機能します。

Q: context.go()context.push() はいつ使い分けますか?

context.go('/path') は、マッチしたルートまでの現在のナビゲーションスタックを置き換えます。context.push('/path') は、既存のスタックの上に新しいルートを追加します。トップレベルのナビゲーション(タブの切り替え、ホームへの移動)には go() を使用し、戻るナビゲーションを保持しながら詳細に移動する場合には push() を使用します。

Q: タブ切り替え時に状態を保持するにはどうしますか?

StatefulShellRoute.indexedStack() を使用します。各ブランチに個別のナビゲーションスタックを維持し、タブ切り替え時にそれらを保持します。これがないと、タブを切り替えるとサブツリー全体が再構築されます。

Q: ルート間で複雑なオブジェクトを渡すにはどうしますか?

3つのアプローチがあります。まず、パスパラメータで ID を渡し、目的の画面でオブジェクトをフェッチします。次に、GoRouter の extra パラメータを使用してオブジェクトを直接渡します。3つ目は、状態管理ソリューション(Riverpod、Bloc)にオブジェクトを格納し、目的地からアクセスします。最初のアプローチは、URL が共有可能なままなので、ディープリンクに最適です。

dart
// Passing via extra (not deep-linkable)
context.push('/product', extra: productObject);

// Receiving via extra
GoRoute(
  path: '/product',
  builder: (context, state) {
    final product = state.extra as Product?;
    return ProductScreen(product: product);
  },
),

Q: GoRouter 17.5 の onEnter コールバックとは何ですか?

onEnter コールバックは、ルートに入るときに実行され、現在と次のルート状態の両方にアクセスできます。ルートが完全にロードされる前にロジックを実行でき、アナリティクスや条件付きプリロードに便利です。

コード生成による型安全ルーティング

GoRouter は、コード生成による型安全なルーティングをサポートしています。ルートデータクラスを定義し、build_runner にナビゲーションコードを生成させます。

routes.dartdart
import 'package:go_router/go_router.dart';

part 'routes.g.dart';

<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

<ProductRoute>(path: '/product/:id')
class ProductRoute extends GoRouteData {
  final String id;
  const ProductRoute({required this.id});

  
  Widget build(BuildContext context, GoRouterState state) =>
      ProductScreen(productId: id);
}

dart run build_runner build を実行後、コンパイル時の安全性でナビゲートできます。

dart
// Type-safe navigation
ProductRoute(id: '123').go(context);

これにより、ルートパスのタイプミスが排除され、パラメータが常に提供されることが保証されます。

本番アプリのための Flutter ナビゲーション習得

  • GoRouter 17.5 は、ディープリンク、ネストされたナビゲーション、または Web サポートを必要とする Flutter アプリに推奨されるナビゲーションソリューションです
  • 永続的なボトムナビゲーションバーには ShellRoute を使用し、各タブのナビゲーションスタックを保持するには StatefulShellRoute を使用します
  • プラットフォーム固有のディープリンク(Android App Links、iOS Universal Links)は GoRouter とは別に設定します。GoRouter はルーティングロジックを処理します
  • 認証ガードは、ルーターレベルまたは個別のルートレベルで redirect コールバックを使用して実装します
  • 面接シナリオでは、宣言的モデルの説明に焦点を当てます:アプリ状態が UI を駆動し、URL は状態から導出され、GoRouter が変換を処理します
  • コード生成による型安全ルートは、タイプミスによるパスのランタイムエラーを排除します
  • バージョン固有の機能については、Flutter ナビゲーションドキュメントGoRouter 変更ログ を確認してください

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

Flutter のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月23日 更新

タグ

#flutter
#gorouter
#navigation
#deep-linking
#mobile

共有

関連記事