【Flutter】go_router_builderを使用してBottomNavigationBarを実装する

## はじめに
go_router_builderを使ってみようということで、使用する利点としては主に以下が挙げられると思います。
- **型安全性の向上**
ルーティングの設定が型安全になり、画面間の移動で必要な情報が正しく渡されているかを、アプリを実行する前にチェックできます。

- **コードの自動生成**
ルーティングに必要なコードが自動で生成され、開発プロセスの効率化が図れます。

この記事では、これらの利点に加え、Riverpodを用いたアプリ全体の状態管理を組み合わせて、ボトムナビゲーションバーの実装をします。

https://pub.dev/packages/go_router_builder

## 環境
- Flutter 3.16.9
- パッケージは以下を参照（初期から追加したもののみ記載しています）

```yaml
dependencies:
  flutter_riverpod: ^2.4.9
  go_router: ^12.1.1

dev_dependencies:
  build_runner: ^2.4.7
  go_router_builder: ^2.3.4
```

## プロジェクト構成
※構成はあくまで一例です。
![スクリーンショット 2024-02-15 15.15.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658262/f2b616f7-c297-05d2-988e-21119ebe18d5.png)

- **main.dart**: アプリケーションのエントリーポイント。ルートウィジェットを定義
- **router**: go_router_builderを使用して定義されたアプリケーションのルートを管理するディレクトリ
  - **branch**: ナビゲーションの分岐を管理するディレクトリ
    - **home_branch.dart**: ホーム画面へのナビゲーションフローを定義
    - **setting_branch.dart**: 設定画面へのナビゲーションフローを定義
  - **routes.dart**: アプリケーションの全ルートを集約するファイル
※後に説明しますが、go_router_builderを使用してルートを定義し、build_runnerによってroutes.g.dartにコードが生成されます。
- **pages**: アプリケーションの各画面を定義するファイルが格納

## 実装
### 1. ボトムナビゲーションの設定とルーティングの構築
ボトムナビゲーションとそれに伴うルーティングの設定は、`routes.dart`に記述していきます。このファイルでは、アプリケーションの主要なナビゲーションパスと、それぞれのパスに対応する画面を定義します。
#### (1) まずはroutes.dartの全コード
```routes.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

part 'routes.g.dart';

part 'branch/home_branch.dart';

part 'branch/setting_branch.dart';

final _rootNavigatorKey = GlobalKey<NavigatorState>();

final routerProvider = Provider<GoRouter>((ref) {
  return GoRouter(
    initialLocation: '/home', // 初期ルート
    navigatorKey: _rootNavigatorKey, // ルートナビゲーターキー
    debugLogDiagnostics: kDebugMode, // デバッグモードでのみログを出力
    routes: $appRoutes, // 生成されたルート
  );
});

// メインシェルルート定義
@TypedStatefulShellRoute<MainShellRouteData>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    homeStatefulShellBranch,
    settingStatefulShellBranch,
  ],
)

// メインシェルルートの状態
class MainShellRouteData extends StatefulShellRouteData {
  const MainShellRouteData();

  @override
  Widget builder(
    BuildContext context,
    GoRouterState state,
    StatefulNavigationShell navigationShell,
  ) {
    return AppNavigationBar(navigationShell: navigationShell);
  }
}

// ナビゲーションバー
class AppNavigationBar extends StatelessWidget {
  const AppNavigationBar({
    super.key,
    required this.navigationShell,
  });

  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        selectedIndex: navigationShell.currentIndex,
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home),
            label: 'home',
          ),
          NavigationDestination(
            icon: Icon(Icons.settings),
            label: 'settings',
          ),
        ],
        onDestinationSelected: _goBranch,
      ),
    );
  }

  // タブ選択時の処理
  void _goBranch(int index) {
    navigationShell.goBranch(
      index,
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}
```

#### (2) 解説
##### ① GoRouterの設定
```routes.dart
final _rootNavigatorKey = GlobalKey<NavigatorState>();

final routerProvider = Provider<GoRouter>((ref) {
  return GoRouter(
    initialLocation: '/home',
    navigatorKey: _rootNavigatorKey,
    debugLogDiagnostics: kDebugMode,
    routes: $appRoutes,
  );
});
```
まず、アプリケーションのナビゲーションを管理するためにGoRouterインスタンスを作成します。このインスタンスは、アプリケーションのルートとなるナビゲーションロジックを定義するために使用されます。


##### ② StatefulShellRouteの追加
```routes.dart
// メインシェルルート定義
@TypedStatefulShellRoute<MainShellRouteData>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    homeStatefulShellBranch,
    settingStatefulShellBranch,
  ],
)

class MainShellRouteData extends StatefulShellRouteData {
  // 省略...
}
```
`@TypedStatefulShellRoute`アノテーションを使用して、アプリケーションのメインナビゲーションシェルを定義しています。

:::note info
`@TypedStatefulShellRoute`アノテーションは、アプリケーションにおけるナビゲーションの構造を定義するために使用されます。
このアノテーションを使って指定された部分は、アプリのメインナビゲーション（画面間の移動を管理する中心的な部分）として機能します。
:::

:::note warn
`@TypedStatefulShellRoute`アノテーションは、必ずStatefulShellRouteDataを拡張するクラスの直前に記述してください。go_router_builderパッケージは、このアノテーションが適用された直下のクラスを基にコードを自動生成します。アノテーションの位置を間違えると、正しくコードが生成されないため注意が必要です。
:::

`branches`プロパティには、ボトムナビゲーションバーの各タブに対応するナビゲーションの分岐を指定します。
ここでの`homeStatefulShellBranch`と`settingStatefulShellBranch`は、それぞれホーム画面と設定画面へのナビゲーションフローを管理する分岐を表しています。

これらの分岐は、home_branch.dartとsetting_branch.dartといった別のファイルでモジュール化しています。後ほど、これらの分岐の設定について詳しく説明します。

:::note info
`<TypedStatefulShellBranch<StatefulShellBranchData>>`は、アプリケーションのナビゲーションフローの中で、特定のナビゲーションパス（例えばホーム画面や設定画面への遷移）を管理するための「分岐」を定義するために使用されます。

TypedStatefulShellBranchは、ナビゲーションの分岐を表しており、
StatefulShellBranchDataは、それぞれの「分岐」が持つべき情報や状態を定義します。
例えば、ホーム画面への分岐では、ホーム画面特有の情報を、設定画面への分岐では、設定画面特有の情報を持たせることができます。
:::



##### ③ ボトムナビゲーションバーの実装
StatefulShellRoute内で、NavigationBarを実装したウィジェットをルート画面として配置します。
```routes.dart
class MainShellRouteData extends StatefulShellRouteData {
  const MainShellRouteData();

  @override
  Widget builder(
    BuildContext context,
    GoRouterState state,
    StatefulNavigationShell navigationShell,
  ) {
    return AppNavigationBar(navigationShell: navigationShell);
  }
}

// ナビゲーションバー
class AppNavigationBar extends StatelessWidget {
  const AppNavigationBar({
    super.key,
    required this.navigationShell,
  });

  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        selectedIndex: navigationShell.currentIndex,
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home),
            label: 'home',
          ),
          NavigationDestination(
            icon: Icon(Icons.settings),
            label: 'settings',
          ),
        ],
        onDestinationSelected: _goBranch,
      ),
    );
  }

  // タブ選択時の処理
  void _goBranch(int index) {
    navigationShell.goBranch(
      index,
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}
```

##### ④ partの設定
partキーワードを使用することで、Dartでは複数のファイルを1つのライブラリとしてグループ化することができます。
```routes.darg
part 'routes.g.dart';

part 'branch/home_branch.dart';

part 'branch/setting_branch.dart';
```
- `part 'routes.g.dart';`
routes.dartファイルにroutes.g.dartファイルを含めることを示しています。(build_runnerコマンドを実行することで自動的に生成されるファイル)
- `part 'branch/home_branch.dart';`
ホーム画面に関連するナビゲーションの分岐（branch）を定義するファイルです。
- `part 'branch/setting_branch.dart';`
設定画面に関連するナビゲーションの分岐を定義するファイルです。


### 2. ホームタブのナビゲーション設定
`home_branch.dart`はアプリケーションのホームタブに関連するナビゲーションロジックを定義する場所です。
このファイルでは、ホーム画面へのルートと、ホーム画面から派生するサブページへのルートが設定されています。

#### (1) home_branch.dartの全コード
```home_branch.dart
part of '../routes.dart';

// ホームタブの定義
class HomeShellBranch extends StatefulShellBranchData {
  const HomeShellBranch();
}

// ホームタプの状態
const homeStatefulShellBranch = TypedStatefulShellBranch<HomeShellBranch>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<HomePageRoute>(
      path: '/home',
      routes: [
        TypedGoRoute<Sample1PageRoute>(
          path: 'sample1',
        ),
        TypedGoRoute<Sample2PageRoute>(
          path: 'sample2',
        ),
      ],
    ),
  ],
);

class HomePageRoute extends GoRouteData {
  const HomePageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const HomePage();
  }
}

class Sample1PageRoute extends GoRouteData {
  const Sample1PageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample1Page();
  }
}

class Sample2PageRoute extends GoRouteData {
  const Sample2PageRoute();

  static final GlobalKey<NavigatorState> $parentNavigatorKey =
      _rootNavigatorKey;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample2Page();
  }
}
```

#### (2) 解説
##### ①　HomeShellBranchの定義
```home_branch.dart
// ホームタブの定義
class HomeShellBranch extends StatefulShellBranchData {
  const HomeShellBranch();
}
```
HomeShellBranchクラスは、ホームタブのナビゲーション分岐を表します。
このクラスは`StatefulShellBranchData`を継承しており、ホームタブに関連するナビゲーションの状態を管理します。

##### ② 各ページのルート定義
```home_branch.dart
class HomePageRoute extends GoRouteData {
  const HomePageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const HomePage();
  }
}

class Sample1PageRoute extends GoRouteData {
  const Sample1PageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample1Page();
  }
}

class Sample2PageRoute extends GoRouteData {
  const Sample2PageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample2Page();
  }
}
```
HomePageRoute、Sample1PageRoute、Sample2PageRouteクラスは、それぞれホーム画面、サンプルページ1、サンプルページ2へのルートを定義しています。
これらのクラスは`GoRouteData`を継承しており、buildメソッド内で対応するウィジェット（ページ）を返します。これにより、指定されたパスにナビゲートされたときに、適切な画面が表示されるようになります。

##### ③ ホームタブのルーティング設定
```home_branch.dart
// ホームタプの状態
const homeStatefulShellBranch = TypedStatefulShellBranch<HomeShellBranch>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<HomePageRoute>(
      path: '/home',
      routes: [
        TypedGoRoute<Sample1PageRoute>(
          path: 'sample1',
        ),
        TypedGoRoute<Sample2PageRoute>(
          path: 'sample2',
        ),
      ],
    ),
  ],
);
```
homeStatefulShellBranchは、ホームタブのルーティングを定義する定数です。
`TypedStatefulShellBranch`を使用して、ホーム画面へのルート(/home)と、ホーム画面から派生するサブページ（sample1、sample2）へのルートを設定しています。

### 3. 設定タブのナビゲーション設定
こちらについてはほとんどホームタブと同じなので、説明は割愛します。
```setting_branch.dart
part of '../routes.dart';

// ホームタブの定義
class SettingShellBranch extends StatefulShellBranchData {
  const SettingShellBranch();
}

// ホームタプの状態
const settingStatefulShellBranch = TypedStatefulShellBranch<SettingShellBranch>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<SettingPageRoute>(
      path: '/setting',
    ),
  ],
);

class SettingPageRoute extends GoRouteData {
  const SettingPageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const SettingPage();
  }
}
```

### 4. コードの自動生成
以下でコードを自動生成します。
これにより、routes.g.dartにコードが生成されます。
```zsh
flutter pub run build_runner build --delete-conflicting-outputs
```

### 5. ルートウィジェットを定義
routerConfigプロパティにGoRouterインスタンスを渡すことで、アプリケーションのルーティング設定が適用されます。
```main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_sample/router/routes.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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,
    );
  }
}
```

### 6. ページ遷移
go_router_builderを使用したことにより、`Sample1PageRoute().push<void>(context);`のようなシンプルなコードでページ遷移を行うことができるようになりました。
```dart
import 'package:flutter/material.dart';
import 'package:go_router_sample/router/routes.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                const Sample1PageRoute().push<void>(context);
              },
              child: const Text('Sample Page 1'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                const Sample2PageRoute().push<void>(context);
              },
              child: const Text('Sample Page 2'),
            ),
          ],
        ),
      ),
    );
  }
}

```

## ボトムナビゲーションバーの表示制御
アプリケーション内でボトムナビゲーションバーを隠す画面と隠さない画面を出し分けることも可能です。GlobalKey<NavigatorState>を活用することで、このような挙動の制御が可能になります。

### 実装
例えばhome_branch.dartのSample2PageRouteに以下の行を追加します。
```diff_dart
class Sample1PageRoute extends GoRouteData {
  const Sample1PageRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample1Page();
  }
}

class Sample2PageRoute extends GoRouteData {
  const Sample2PageRoute();

+ static final GlobalKey<NavigatorState> $parentNavigatorKey = _rootNavigatorKey;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const Sample2Page();
  }
}
```

この`$parentNavigatorKey`は、Sample2Pageが表示される際にメインのナビゲーションスタック（_rootNavigatorKeyに紐づいているもの）を参照することを示しています。

これにより、Sample2Pageが表示される際には、メインのナビゲーションスタックが使用され、その結果としてボトムナビゲーションバーが隠される挙動を実現できます。

コードの自動生成を忘れずに。
```zsh
flutter pub run build_runner build --delete-conflicting-outputs
```

## 挙動
| 通常 | ボトムナビゲーションバーを隠す |
| :------------: | :------------: |
| <img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658262/7daee3eb-40c5-703a-3a64-1f66a75f913a.gif" /> | <img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658262/2ed00b7c-85a0-72ac-8e38-113ef81a6782.gif" /> |


## 参考
https://codewithandrea.com/articles/flutter-bottom-navigation-bar-nested-routes-gorouter/

https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_with_keys_example.dart