Go Router 入門

## 0. 概要
今まで`Navigator.push()`や`Navigator.pop()`をメインに使用していたのですが、最近`go_router`をついに使用しました。
正直、Navigator.pushとpopで十分じゃないか...いいじゃないか...と思い使ってなかったのですが、ついにその時がきてしまったので基本的な事を調べてサンプルアプリを作成しました。
それに沿った説明をしつつ、メモ代わりとしても残しつつというスタンスでやっていきます。（その方が記憶に定着する）

本記事の内容に関して、不正確な情報や改善点がございましたらコメントいただけますと幸いです！

![画面収録 2025-01-23 21.02.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3282990/16e4088a-3b12-3202-52bd-52ed8ab4e550.gif)
（画質が荒くすみません。高画質にしたんだけどなぁ）

## 1. 準備

### パッケージの追加と依存関係を解決する

```dart
flutter pub add go_router ; flutter pub get
```

### pubspec.yamlの確認

```yaml
dependencies:
  go_router: ^14.6.3 # 最新の安定バージョンを使用
```

## 2. 各ファイル構造

```bash
lib/
├── router/
│   ├── router.dart           # ルーティング設定
│   ├── route_path_name.dart  # パス定義
│   └── app_routes.dart       # 遷移メソッド
├── pages/
│   ├── home_page.dart        # 画面実装
│   ├── detail_page.dart      # 画面実装
│   ├── settings_page.dart    # 画面実装
│   └── profile_page.dart     # 画面実装
└── main.dart                 # エントリーポイント
```

## 3. ファイル構成

### ルーティング設定（router.dart）

- GoRouter の設定
- RoutePathName を使用してパスを参照
- 各画面のビルダー設定

```dart
import 'package:go_router/go_router.dart';
import '../pages/home_page.dart';
import '../pages/detail_page.dart';
import '../pages/settings_page.dart';
import '../pages/profile_page.dart';
import 'route_path_name.dart';

// アプリ全体のルーティング設定
final router = GoRouter(
  // 初期パス
  initialLocation: RoutePathName.home,

  // ルート定義
  routes: [
    // ホーム画面
    GoRoute(
      path: RoutePathName.home,
      name: RoutePathName.homeName,
      builder: (context, state) => const HomePage(),
    ),

    // 詳細画面
    GoRoute(
      path: RoutePathName.detail,
      name: RoutePathName.detailName,
      builder: (context, state) => const DetailPage(),
    ),

    // 設定画面
    GoRoute(
      path: RoutePathName.settings,
      name: RoutePathName.settingsName,
      builder: (context, state) => const SettingsPage(),
    ),

    // プロフィール画面
    GoRoute(
      path: RoutePathName.profile,
      name: RoutePathName.profileName,
      builder: (context, state) => const ProfilePage(),
    ),
  ],
);
```

### パス管理（route_path_name.dart）

- パスと名前を定数として一元管理
- インスタンス化を防ぐ為に private コンストラクタを使う

```dart
// パス名を管理する定数クラス
class RoutePathName {
  RoutePathName._(); // インスタンス化を防ぐ

  // パス
  static const String home = '/';
  static const String detail = '/detail';
  static const String settings = '/settings';
  static const String profile = '/profile';

  // ルート名
  static const String homeName = 'home';
  static const String detailName = 'detail';
  static const String settingsName = 'settings';
  static const String profileName = 'profile';
}
```

### 遷移メソッド（app_routes.dart）

- RoutePathName を使用してパスを参照

```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'route_path_name.dart';

// コールバック型の定義
typedef NavigationCallback = void Function(BuildContext context);

class AppRoute {
  AppRoute._();

  // push
  static void goToHome(BuildContext context) => context.go(RoutePathName.home);

  static void goToDetail(BuildContext context) =>
      context.push(RoutePathName.detail);

  static void goToSettings(BuildContext context) =>
      context.push(RoutePathName.settings);

  static void goToProfile(BuildContext context) =>
      context.push(RoutePathName.profile);

  // pop
  static void pop(BuildContext context) => context.pop();
}
```

### 画面実装（各ページファイル）

<details><summary>lib/pages/home_page.dart</summary>

```dart
import 'package:flutter/material.dart';
import '../router/app_routes.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
        backgroundColor: Colors.blue,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => AppRoute.goToDetail(context),
              child: const Text('詳細ページへ'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => AppRoute.goToSettings(context),
              child: const Text('設定ページへ'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => AppRoute.goToProfile(context),
              child: const Text('プロフィールページへ'),
            ),
          ],
        ),
      ),
    );
  }
}
```
</details>
    
<details><summary>lib/pages/profile_page.dart</summary>

```dart
import 'package:flutter/material.dart';
import '../router/app_routes.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('プロフィール'),
        backgroundColor: Colors.purple,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircleAvatar(
              radius: 50,
              child: Icon(Icons.person, size: 50),
            ),
            const SizedBox(height: 20),
            const Text('プロフィールページ', style: TextStyle(fontSize: 24)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => AppRoute.pop(context),
              child: const Text('戻る'),
            ),
          ],
        ),
      ),
    );
  }
}
```
</details>
    
<details><summary>lib/pages/detail_page.dart</summary>

```dart
import 'package:flutter/material.dart';
import '../router/app_routes.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('詳細'),
        backgroundColor: Colors.green,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('詳細ページ', style: TextStyle(fontSize: 24)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => AppRoute.pop(context),
              child: const Text('戻る'),
            ),
          ],
        ),
      ),
    );
  }
}
```
</details>

<details><summary>lib/pages/settings_page.dart</summary>

```dart
import 'package:flutter/material.dart';
import '../router/app_routes.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('設定'),
        backgroundColor: Colors.orange,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('設定ページ', style: TextStyle(fontSize: 24)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => AppRoute.pop(context),
              child: const Text('戻る'),
            ),
          ],
        ),
      ),
    );
  }
}
```
</details>
    

### アプリケーションのエントリーポイント（main.dart）

```dart
import 'package:flutter/material.dart';
import 'router/router.dart'; // go_routerの設定ファイルをインポート

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

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

  @override
  Widget build(BuildContext context) {
    // MaterialApp から MaterialApp.router に変更することで、go_routerによるルーティング機能を有効化
    return MaterialApp.router(
      title: 'Go Router Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      // router/router.dartで定義したルーティング設定を適用
      // これによりアプリ全体でgo_routerの機能が使用可能になる
      routerConfig: router,
    );
  }
}

```

## 4. ルーティング関連ファイルの役割と関係

### RoutePathName（route_path_name.dart）

```dart
class RoutePathName {
  static const String detail = '/detail';  // パスの定義

  // ... existing code ...
}
```

- パス文字列を定数として管理することで、タイプミス防止になる
- パスの一元管理が可能なため、コードの保守が楽

### AppRoute（app_routes.dart）

```dart
class AppRoute {
  static void pushToDetail(BuildContext context) =>
      context.push(RoutePathName.detail);  // 遷移処理の実装

      // ... existing code ...
}
```

- 遷移処理のカプセル化
- コンテキストの扱い
- 実際の画面遷移の実行

### Router（router.dart）

```dart
final router = GoRouter(
  initialLocation: RoutePathName.home,
  routes: [
    GoRoute(
      path: RoutePathName.detail,
      builder: (context, state) => const DetailPage(),
    ),

    // ... existing code ...
  ],
);
```

- パスと画面の紐付け
- 初期画面の設定
- GoRouter の基本設定

### 実装パターン

3つのファイルそれぞれの役割

- `RoutePathName`：パスの定義と管理
- `AppRoute`：画面遷移メソッドの提供
- `router`：パスと画面の紐付けと基本設定

```dart
// 1. RoutePathName としてパスを定義
//（ファイル）lib/router/route_path_name.dart
class RoutePathName {
  static const String detail = '/detail';
}

// 2. AppRoute で遷移処理を実装
//（ファイル）lib/router/app_routes.dart
class AppRoute {
  static void goToDetail(BuildContext context) =>
		  context.go(RoutePathName.detail);
}

// 3. パスと画面を紐付け
// （ファイル）lib/router/router.dart
final router = GoRouter(
  routes: [
    GoRoute(
      path: RoutePathName.detail,
      builder: (context, state) => const DetailPage(),
    ),
  ],
);

// 4. 画面での使用
//（ファイル）lib/pages/home.page.dart などの pages フォルダ配下
ElevatedButton(
  onPressed: () => AppRoute.pushToDetail(context),
  child: const Text('詳細へ'),
)
```

## 5. 画面遷移の種類と使い分け

### push遷移

```dart
ElevatedButton(
  onPressed: () => AppRoute.pushToDetail(context),
  child: const Text('詳細へ'),
)
```

スタックに新しい画面を積む

- スタックに新しい画面を積む
- 戻るボタンで前の画面に戻れる
- URLは変更されない

### go遷移

```dart
// 画面のスタックを置き換えるため、戻ることができない
ElevatedButton(
  onPressed: () => AppRoute.goToDetail(context),
  child: const Text('詳細へ'),
)
```

現在の画面を新しい画面に置き換える

- 現在の画面を新しい画面に置き換える
- スタックをクリアして新しい画面を表示
- URLが変更される

### pop遷移

```dart
ElevatedButton(
  onPressed: () => AppRoute.pop(context),
  child: const Text('戻る'),
)
```

## 6. 実装のポイント

遷移するだけなら2つの方法があります。

### カプセル化した AppRoute クラスを使用する方法

```dart
ElevatedButton(
  onPressed: () => AppRoute.goToDetail(context),
  child: const Text('詳細へ'),
)
```

### 直接パスを書く方法

```dart
ElevatedButton(
  onPressed: () => context.go('/detail'),
  child: const Text('詳細へ'),
)
```

### どちらの実装が良いか？

「カプセル化した AppRoute クラスを使用する方法」の方が良いと思います。

例えば、複数ファイルで直接パスを書く方法だと下記の様に、パスのタイプミスやパス変更時に全てのファイルが修正対象になったりコードの保守性が低下します。

```dart
// 画面A.dart
onPressed: () => context.go('/detail'),

// 画面B.dart
onPressed: () => context.go('/detail'),

// 画面C.dart
onPressed: () => context.go('/detial'), // タイプミス！
```

カプセル化してAppRoute クラスを使用した実装はこの様にパスのタイプミスをなくす事が出来て、パスの変更も1箇所で完結します。

```dart
// 画面A.dart
onPressed: () => AppRoute.goToDetail(context),

// 画面B.dart
onPressed: () => AppRoute.goToDetail(context),

// 画面C.dart
onPressed: () => AppRoute.goToDetail(context),
```

カプセル化した方法を使うことで、内部の処理を別ファイルに定義して他のファイルではその処理を呼び出すだけになります。

そのため、将来の変更に強い & 安全なコードにする事が出来ます。

### reference
https://pub.dev/documentation/go_router/14.6.3/

https://pub.dev/packages/go_router