[Flutter] go_routerでbottom navigationの出し入れをしてみる

## 概要

Flutterで[go_router](https://pub.dev/packages/go_router)パッケージを用いた時、bottomNavigationBarの扱いが少し面倒になるそうです。
簡易的ではあるものの、bottomNavigationを残しながら画面遷移し、時には消すような処理を実装していきます。

通常遷移してしまうと当然のように別ページが表示されてしまいますが、go_routerのドキュメントにもある通り、
「navigatorBuilder」を用いれば実現可能です。

https://gorouter.dev/navigator-builder

## 完成イメージ



## 実装

### フォルダ構成
#### Github
https://github.com/kurogoma-flutter/go_router_demo

#### 構成
```
lib
 ∟provider <- BottomNavigationの切り替えと、表示非表示
 ∟screens <- BottomNavigationの土台の画面
 ∟widgets <- BottomNavigationの土台（表示用・非表示用）
 ∟main.dart <- GoRouterの初期設定
 ∟route.dart <- ルーティング設定（BNBの表示切り替えも）
```

## 主要部分

```lib/route.dart
final routerProvider = Provider((ref) {
  return GoRouter(
    initialLocation: '/bottomA',
    routes: <GoRoute>[
      /// BottomNavigationのタブのトップページ
      GoRoute(
        path: '/bottomA',
        pageBuilder: (context, state) => CustomTransitionPage(
            key: state.pageKey,
            child: const BottomNavigationTopA(),
            transitionDuration: Duration.zero,
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) => child),
        /// 同じタブ内で遷移したい画面リストをまとめる
        routes: [
          GoRoute(
            path: 'pageA',
            pageBuilder: (context, state) => MaterialPage(
              key: state.pageKey,
              child: const BottomAPageA(),
            ),
          ),
          GoRoute(
            path: 'pageB',
            pageBuilder: (context, state) => MaterialPage(
              key: state.pageKey,
              child: const BottomAPageB(),
            ),
          ),
          GoRoute(
            path: 'pageC',
            pageBuilder: (context, state) => MaterialPage(
              key: state.pageKey,
              child: const BottomAPageC(),
            ),
      // ...タブの数分上記を書く...
    ],
    /// 常時表示したいウィジェットを定義できる
    navigatorBuilder: (context, state, child) {
      /// state.locationで現在のパスが取れる。
      /// パスがProviderにある非表示リストに合致していれば非表示にする
      if (ref
          .read(hideBottomNavigationProvider)
          .isHideBottomNavigation(state.location)) {
        return Navigator(
          onPopPage: (route, dynamic __) => false,
          pages: [
            MaterialPage<Widget>(
              /// BottomNavigationがない土台
              child: EmptyBottomNavigation(
                child: child,
              ),
            ),
          ],
        );
      } else {
        return Navigator(
          onPopPage: (route, dynamic __) => false,
          pages: [
            MaterialPage<Widget>(
              /// BottomNavigationがある土台
              child: MyBottomNavigation(
                child: child,
              ),
            ),
          ],
        );
      }
    },
  );
});

```

### Provider部分

```lib/provider/bottom_navigation_provider.dart
// BNBを切り替えるだけ
final bottomNavIndexProvider = StateProvider((ref) => 0);
```
```lib/provider/hide_bottom_navigation_provider.dart
final hideBottomNavigationProvider =
    ChangeNotifierProvider<HideBottomNavigation>(
  (ref) {
    return HideBottomNavigation();
  },
);

class HideBottomNavigation extends ChangeNotifier {
  HideBottomNavigation();
  /// 非表示リスト（別ファイルに定義した方が好ましい）
  static const List<String> hideBottomNavigationPageList = [
    '/bottomC/pageA',
    '/bottomC/pageB',
    '/bottomC/pageC',
  ];

  /// [hideBottomNavigationPageList]に含まれる場合BNBを非表示
  bool isHideBottomNavigation(String currentPath) {
    return hideBottomNavigationPageList.contains(currentPath);
  }
}
```

### BottomNavigationの表示

```lib/widgets/bottom_navigation.dart
class MyBottomNavigation extends ConsumerStatefulWidget {
  const MyBottomNavigation({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _MyBottomNavigationState();
}

class _MyBottomNavigationState extends ConsumerState<MyBottomNavigation> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          widget.child, // routerで選択された画面をそのまま表示
          /// シンプルにBottomNavigationを最下部に配置
          Align(
            alignment: Alignment.bottomCenter,
            child: BottomNavigationBar(
              type: BottomNavigationBarType.fixed,
              currentIndex: ref.watch(bottomNavIndexProvider),
              onTap: (i) {
                ref.read(bottomNavIndexProvider.notifier).update((state) => i);

                // indexに応じてGoRouterのページに遷移
                switch (i) {
                  case 0:
                    context.go('/bottomA');
                    break;
                  case 1:
                    context.go('/bottomB');
                    break;
                  case 2:
                    context.go('/bottomC');
                    break;
                }
              },
              items: const [
                BottomNavigationBarItem(
                  icon: Icon(Icons.arrow_forward, color: Colors.grey),
                  label: 'BottomA',
                ),
                BottomNavigationBarItem(
                  icon: Icon(Icons.replay, color: Colors.grey),
                  label: 'BottomB',
                ),
                BottomNavigationBarItem(
                  icon: Icon(Icons.tab, color: Colors.grey),
                  label: 'BottomC',
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
```

### BottomNavigationの非表示

```lib/widgets/empty_bottom_navigation.dart
class EmptyBottomNavigation extends ConsumerStatefulWidget {
  const EmptyBottomNavigation({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _EmptyBottomNavigationState();
}

class _EmptyBottomNavigationState extends ConsumerState<EmptyBottomNavigation> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: widget.child,
    );
  }
}

```

以上です！
まだ深く踏み込んだわけではないのでこれがどんな弊害があるか理解しきれていませんが、
複雑なUIでなければ実装可能かと思います！