⚙️

Flutter初心者のためのGoRouterパッケージ

2024/03/31に公開

Flutterのナビゲーションにはたくさんの選択肢がありますが、今回はGoRouterパッケージについて解説します。GoRouterはFlutterの公式が推奨するナビゲーション管理パッケージで、柔軟性に富んだルーティングを実現できます。

GoRouterの基本的な実装流れ

  1. パッケージ追加

pubspec.yamlファイルにgo_routerの依存関係を追加します。

dependencies:
  go_router: ^6.0.1
  1. ルート情報を定義

routesリストにページごとのルート情報を定義します。pathでURLパスを指定し、builderで対応する画面ウィジェットを返します。

// router.dart
final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) => HomeScreen(),
    ),
    GoRoute(
      path: '/details',
      builder: (BuildContext context, GoRouterState state) => DetailsScreen(),
    ),
  ],
);
  1. アプリケーションのメインルートでGoRouterを設定

MaterialApp.routerコンストラクタを使って、GoRouterの設定を行います。

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

  
  Widget build(BuildContext context) {
    return MaterialApp.router( // routerを設定
      routerConfig: router,
      theme: ThemeData(
        appBarTheme: AppBarTheme(
          color: Colors.amber[400],
        ),
      ),
    );
  }
}
  1. onPressedでcontext.go('/path')を呼び出して画面遷移

ボタンのonPressedなどで、context.go('/path')を呼び出すことで画面遷移ができます。

ElevatedButton(
  onPressed: () => context.go('/details'),
  child: Text('Go to Details'),
),

GoRouterの高度な機能

GoRouterには基本的な画面遷移だけでなく、高度なナビゲーション機能がたくさんあります。公式ドキュメントのNavigationを参照すると、さまざまな種類の遷移について学ぶことができます。

  • context.go:単純な画面遷移
  • context.push:スタックに積む遷移
  • context.pushNamed:名前付きルートへの遷移
  • ShellRoute:アプリの骨格を構成するルート

特にShellRouteは便利で、アプリ全体で共通するUI(AppBarなど)を一か所で管理できるようになります。

GoRouterの設定方法

GoRouterの設定には主に2つの方法があります。

  1. routerDelegaterouterInformationParserを渡す方法
MaterialApp.router(
  routerDelegate: router.routerDelegate,
  routerInformationParser: router.routerInformationParser,
)
  1. routerConfigにGoRouterインスタンスを渡す方法(推奨)
MaterialApp.router(
  routerConfig: router,
)

StatefulShellRouteを使ったBottomNavigationBarの永続表示

BottomNavigaitonBarは、StatefulShellRoute.indexStackを使用することで、比較的簡単に実装ができます。

実装する上で、理解しておく必要があるものを書いておくと、以下5つになります。

  1. final _rootNavigatorKey = GlobalKey<NavigatorState>();
  2. StatefulShellRoute.indexedStack
  3. StatefulShellRoute.indexedStackのbranches
  4. StatefulShellBranch
  5. StatefulShellBranchのGoRoute

字面だけなのでイメージしづらかったのですが、この方が作成しているイメージ図が分かりやすかったので、載せておきます。

https://zenn.dev/flutteruniv_dev/articles/stateful_shell_route

僕が持っているイメージとしては、以下です。

  1. StatefulShellRoute.indexedStackで、各タブを管理する箱を作る
  2. indexedStackのbranchesで、各タブ内で独立した箱を作る
  3. StatefulShellBranchのGoRouteで、独立した箱の中での画面遷移を実現する

このイメージを持ちつつ実装していきます。

1. ルートナビゲーターキーの定義

最初に、アプリケーションのナビゲーション状態を管理するためのルートナビゲーターキーを作成します。

final _rootNavigatorKey = GlobalKey<NavigatorState>();
  • このキーは、アプリのナビゲーションスタック全体を管理するために使われます。
  • アプリがこの「どの画面に行ったかの履歴」をきちんと管理し、ユーザーが簡単に前に戻ったり、新しい画面に進んだりできるようにすることを意味してます。
  • このルートナビゲーターキーを使うことで、アプリ全体のナビゲーション状態を一元的に管理できるようになります。
  • 逆にルートナビゲーターキーを定義しないと、ユーザーが前の画面に戻ろうとしたときに、適切に遷移できなくなる可能性が出てきます。

2. StatefulShellRoute.indexedStack

次に、StatefulShellRoute.indexedStackを使用して、複数のナビゲーションブランチを持つ箱を作成します。

StatefulShellRoute.indexedStack(
  builder: (context, state, navigationShell) {
    return TodoBottomNavigationBar(navigationShell: navigationShell);
  },
  branches: [ ... ],
)
  • TodoBottomNavigationBarはアプリの下部にあるナビゲーションバーのことです。navigationShellを通じてナビゲーションを管理します。コードを載せておくと、以下です。
  • ボトムナビゲーションバー専用のクラスを作成して、それをアプリ全体で表示させます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class TodoBottomNavigationBar extends StatelessWidget {
  const TodoBottomNavigationBar({
    super.key,
    required this.navigationShell,
  });

  final StatefulNavigationShell navigationShell;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        onTap: (selectedIndex) {
          // ここでcurrentIndexを更新し、適切な画面に遷移する
          navigationShell.goBranch(
            selectedIndex,
            initialLocation: selectedIndex == navigationShell.currentIndex,
          );
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
          BottomNavigationBarItem(icon: Icon(Icons.add), label: 'add'),
        ],
      ),
    );
  }
}

3. StatefulShellRoute.indexedStackのbranches

indexedStack内で、異なるナビゲーションセクション(branches)、いわゆる「各タブ内で独立した箱」を定義します。

branches: [
  StatefulShellBranch(...),
  StatefulShellBranch(...),
]
  • StatefulShellBranchはアプリの一部分を表し、それぞれ独立したナビゲーションスタックを持ちます。
  • 各ブランチは独立したナビゲーションスタックを持っているため、ブランチ間で直接データを受け渡すことはできません。
  • 必要に応じて、ブランチ間でデータを共有するための状態管理の仕組み(例えば、RiverpodなどのStateProvider)を導入する必要が出てきます。
  • ブランチ間の遷移は、context.pushNamed()context.goNamed()などのメソッドを使います。

4. StatefulShellBranch

StatefulShellBranchを使用して、ナビゲーションブランチを具体的に定義します。

StatefulShellBranch(
  navigatorKey: _shellNavigatorHomeKey,
  routes: [ ... ],
)
  • ここでnavigatorKeyは、そのブランチのナビゲーションを一意に識別するために使用されます。

5. StatefulShellBranchのGoRoute

最後に、各StatefulShellBranch内でGoRouteを定義して、具体的なナビゲーションパスとそれに対応する画面を設定します。

GoRoute(
  path: '/',
  builder: (BuildContext context, GoRouterState state) => const TodoListPage(),
)
  • ここで、pathはナビゲーションのパスを指し、builderはそのパスにアクセスしたときに表示されるウィジェットを生成します。ここではTodoListPageを返しています。

全体コード

これでアプリ全体にBottomNavigationBarを表示させることができたはずです。
各タブが独立しているので、別タブをタップして前のタブに再び戻ったときも、前表示されていた状態が維持されていると思います。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:todo_riverpod/entities/todo_item.dart';
import 'package:todo_riverpod/todo_bottom_navigation_bar.dart';
import 'package:todo_riverpod/ui/add_todo_page.dart';
import 'package:todo_riverpod/ui/todo_list_page.dart';
import 'package:todo_riverpod/ui/update_todo_page.dart';

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorHomeKey = GlobalKey<NavigatorState>(debugLabel: 'home');
final _shellNavigatorAddKey = GlobalKey<NavigatorState>(debugLabel: 'add');

final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/',
  routes: <RouteBase>[
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return TodoBottomNavigationBar(navigationShell: navigationShell);
      },
      branches: [
        // homeブランチ
        StatefulShellBranch(
          navigatorKey: _shellNavigatorHomeKey,
          routes: <RouteBase>[
            GoRoute(
              path: '/',
              builder: (BuildContext context, GoRouterState state) =>
                  const TodoListPage(),
            ),
            GoRoute(
              path: '/updateTodo',
              builder: (context, state) {
                final todoItem =
                    state.extra as TodoItem? ?? TodoItem(todoId: -1);
                return UpdateTodoPage(todoItemProps: todoItem);
              },
            ),
          ],
        ),
        // addブランチ
        StatefulShellBranch(
          navigatorKey: _shellNavigatorAddKey,
          routes: <RouteBase>[
            GoRoute(
              path: '/addTodo',
              builder: (BuildContext context, GoRouterState state) =>
                  const AddTodoPage(),
            ),
          ],
        ),
      ],
    ),
  ],
);

上記の手順を踏むことで、BottomNavigationBarを使ったナビゲーションをFlutterアプリに実装できます。各ブランチは独立しているため、ユーザーがアプリ内で異なるセクションに移動しても、それぞれのナビゲーション状態は保持されるので、そこが嬉しいですね。

Discussion