❄️

初心者がgo_routerのStatefulShellRouteでボトムナビの実装に挑戦する

今回記事を書いた経緯

最近Flutterのアプリ開発を始めた駆け出しエンジニアです。
社内でアプリ開発の勉強をしてみたいメンバーを有志で集め、チームで簡単な記事共有アプリを作ってます!

今回は、最近アプリ開発を始めた人向けに、アプリの骨組みになる遷移ライブラリであるgo_routerと今回採用するStatefulShellRouteの使い方についてまとめてみました。

検証に使用するアプリイメージ

まずは、実際に作ってみるプロダクトのイメージをAIに作ってもらいました。以下のようなシンプルなボトムナビゲーションを実装する際に使用するのが今回の題材としているStatefulShellRouteというパッケージです。

画面ごとの役割

今回のアプリケーションでボトムナビゲーションに定義している各画面の役割は以下のようなイメージです。

画面 それぞれの役割
ホーム ZennやQiitaなどの技術記事投稿サイトからWebAPI経由で任意のコンテンツを一覧表示する画面
検索 気になる記事をキーワードないしはカテゴリタグを設定することで絞り込みする画面
追加 自分でスマホから作成した記事を特定のプラットフォーム向けに投稿する画面
マイページ 自分がお気に入りしている記事や投稿した記事の件数サマリ、ユーザー情報を確認する画面
設定 利用規約やアカウントの情報変更など、各種設定を閲覧する画面

構造のイメージ

それぞれの画面のページ構成は以下のような階層構造に基づいて、定義されるため、各画面の遷移元となる親要素をStatefulShellRouteで定義してあげることで各画面からどこのページに遷移するかが明確になります。

    └── StatefulShellRoute
        ├── StatefulShellBranch # homeブランチ
        │   └── GoRoute # detail page
        ├── StatefulShellBranch # searchブランチ
        │   └── GoRoute # detail page
        ├── StatefulShellBranch # addブランチ
        │   └── GoRoute # crate page
        └── StatefulShellBranch # profileブランチ
        │    └── GoRoute # profile page
        └── StatefulShellBranch # settingブランチ
            └── GoRoute # setting page
                └── GoRoute # acount detail page

前談:go_routerについて

概要:go_routerは、Flutterアプリケーションのルーティングをシンプルかつ宣言的に管理するためのパッケージです。WebのURLベースのルーティングに似た設計思想を持っており、ディープリンクやブラウザのナビゲーション(戻る/進む)に対応しています。

Flutterの公式ドキュメントでも一番最初に紹介されているパッケージであるgo_routerを使うことで、ナビゲーションを必要とするユースケースに基本的には対応できるようになるみたいです。
https://docs.flutter.dev/ui/navigation#using-the-router
https://pub.dev/packages/go_router

go_routerの主な特徴

  1. URLベースのルーティング:
    アプリ内の各画面が固有のURLを持つように設定します。これにより、WebアプリケーションのようにURLで特定の画面に直接アクセスできます。
  2. 宣言的なAPI:
    ウィジェットツリーの中でナビゲーションの状態を宣言的に管理します。これにより、ウィジェットのビルド時にルーティングの変更を検知して自動的にナビゲーションを更新できます。
  3. 深いリンク(ディープリンク)対応:
    アプリが起動していない状態でも、外部のURLから特定の画面に直接遷移させることが可能です。これは、プッシュ通知やWebサイトからの誘導に役立ちます。
  4. リダイレクト機能:
    ユーザーの認証状態など、特定の条件に応じて自動的に別のルートにリダイレクトさせることができます。例えば、未ログインのユーザーが認証が必要な画面にアクセスしようとした場合、ログイン画面に自動的に遷移させるといったことが可能です。
  5. ネストされたルーティング:
    画面の中にさらに画面をネストして表示するような、複雑なレイアウトを簡単に実現できます。これにより、タブバー内の画面遷移などを効率的に管理できます。

パッケージのインストール

まずはプロジェクトにgo_routerを追加します。ターミナルで以下のコマンドを実行してください。

flutter pub add go_router

これでpubspec.yamlに以下のように追加されていればOKです。

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.0 # バージョンはよしなに

StatefulShellRouteについて

今回採用するStatefulShellRouteクラスは、go_routerの中でもボトムナビゲーションのような同一のルートパスに紐づいて複数のページに遷移し、画面表示するナビゲーターの作成に役立ちます。導入することで、各画面の遷移時にボトムのナビゲーションウィジェットを再作成せずに遷移することができます。(言い換えると、ボトムナビを固定したまま中身のブランチページだけ遷移するような構造)

StatefulShellRouteのコード解説

実装1:main.dartの設定
go_routerを使用する場合、main.dart(アプリのエントリーポイント)の書き方が通常のアプリと少し異なります。MaterialAppの代わりにMaterialApp.routerコンストラクタを使用します。

main
import 'package:flutter/material.dart';
import 'router.dart'; // 定義したrouterをインポート

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

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

  
  Widget build(BuildContext context) {
    // MaterialApp.routerを使用するのがポイント
    return MaterialApp.router(
      routerConfig: router, // ここにGoRouterの設定を渡す
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
    );
  }
}

実装2:routes.dartの全体実装イメージ
それぞれのブロックに分けてコードを解説します。

routes
// GoRouterの設定をトップレベルで定義します。
final GoRouter router = GoRouter(
  // 初期表示ルートを `/home` に設定します。
  initialLocation: '/home',
  routes: [
    // StatefulShellRouteを使用して、永続的なUI(BottomNavigationBar)を持つ画面を構築します。
    StatefulShellRoute.indexedStack(
        builder: (context, state, navigationShell) {
          // UIシェルとしてMainNavigationScreenウィジェットを返します。
          return MainNavigationPage(navigationShell: navigationShell);
        },
        branches: [
          // ホームタブのブランチ
          StatefulShellBranch(
            routes: [
              GoRoute(
                path: '/home',
                builder: (BuildContext context, GoRouterState state) {
                  return const HomePage();
                },
              )
            ],
          ),
          // 検索タブのブランチ
          StatefulShellBranch(
            routes: [
              GoRoute(
                path: '/search',
                builder: (BuildContext context, GoRouterState state) {
                  return const SearchPage();
                },
              )
            ],
          ),
            ...

1.アプリケーション全体のルーティングを司る GoRouter インスタンスを定義

final GoRouter router = GoRouter(
  // 初期表示ルートを `/home` に設定します。
  initialLocation: '/home',
  routes: [
    // ...
  ],
);

final GoRouter router = ...:

go_router パッケージの核となるルーターオブジェクトを作成しています。

initialLocation: '/home':

アプリが起動したとき、またはディープリンクがない場合に**最初に表示されるURL(ルート)**を指定しています。ここでは、アプリの顔となる /home(ホーム画面)を初期ルートに設定しています。

2.StatefulShellRouteによる永続UI(シェル)の定義

// StatefulShellRouteを使用して、永続的なUI(BottomNavigationBar)を持つ画面を構築します。
 StatefulShellRoute.indexedStack(
        builder: (context, state, navigationShell) {
          // UIシェルとしてMainNavigationScreenウィジェットを返します。
          return MainNavigationPage(navigationShell: navigationShell);
        },
        branches: [

これが、今回の記事の核となる部分です。StatefulShellRoute.indexedStack を使うことで、状態を維持するタブ構成(ボトムナビゲーションなど)を定義します。

StatefulShellRoute.indexedStack:

go_routerが内部的にIndexedStack ウィジェットを使用して、複数の子ルート(タブ)を切り替えるよう指示しています。このIndexedStackのおかげで、ユーザーがタブを切り替えても、各タブ内のウィジェットの状態やナビゲーション履歴が保持されます。

3. StatefulShellBranchによる各タブ(ブランチ)の定義

// ホームタブのブランチ
StatefulShellBranch(
  routes: [
    GoRoute(
      path: '/home',
      builder: (BuildContext context, GoRouterState state) {
        return const HomePage();
      },
    )
  ],
),

StatefulShellBranch は、シェルの子となるナビゲーションスタックの単位を定義します。それぞれのブランチが、ボトムナビゲーションの一つのタブに対応します。

今回導入してみてわかったこと

自分が忘れないようにと思いメモとして記事投稿しました!

今回まとめたStatefulShellRouteを採用することで各タブごとの遷移とタブ内での遷移を明確に切り分けることができ、ユーザー体験としてもとても明確で整理された遷移体験が生まれると感じました。

また、開発体験としても各ブランチごとにルーティングを設定するため、深くネストされたルーティング管理にならず可読性の高いコードになることがわかりました。状態管理も各タブ感の遷移でそこまで考慮しなくてもOKになるので、遷移時の処理を個別に書かなくて済むのはかなりコスパ良くナビゲーション設定できる部分かと思います。

ということで、今回はStatefulShellRouteについて書いてみました!
初心者の方もアプリ作る際はぜひ参考にしてみてください!応用編はもし機会があれば書いてみます。

基本的にはFlutterの公式ドキュメントをベースに記事を作成しました↓
https://docs.flutter.dev/ui/navigation#using-the-router

Accenture Japan (有志)

Discussion