6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-02-15

はじめに

go_router_builderを使ってみようということで、使用する利点としては主に以下が挙げられると思います。

  • 型安全性の向上
    ルーティングの設定が型安全になり、画面間の移動で必要な情報が正しく渡されているかを、アプリを実行する前にチェックできます。

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

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

環境

  • Flutter 3.16.9
  • パッケージは以下を参照(初期から追加したもののみ記載しています)
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

  • 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アノテーションを使用して、アプリケーションのメインナビゲーションシェルを定義しています。

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

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

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

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

<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にコードが生成されます。

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);のようなシンプルなコードでページ遷移を行うことができるようになりました。

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を活用することで、このような挙動の制御が可能になります。

実装

例えばhome_branch.dartのSample2PageRouteに以下の行を追加します。

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が表示される際には、メインのナビゲーションスタックが使用され、その結果としてボトムナビゲーションバーが隠される挙動を実現できます。

コードの自動生成を忘れずに。

flutter pub run build_runner build --delete-conflicting-outputs

挙動

通常 ボトムナビゲーションバーを隠す

参考

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?