📲

【Flutter】auto_routeパッケージで画面遷移を実装してみた

2024/10/30に公開

はじめに

記事の目的と概要

本記事では、auto_routeパッケージのルーティング定義と画面遷移の仕方の実装例を解説します。(自身がキャッチアップした内容を発信するために書いていますので、もし間違っていたりしたら優しく指摘してくださると嬉しいです🙇‍♂️)

auto_routeとは

auto_routeは、タイプセーフな引数の受け渡しとディープリンクを可能にし、コード生成を活用してボイラープレートを最小限に抑えられるFlutter向けのナビゲーションパッケージです。開発者にとって次のような特徴を持つため、ナビゲーションの管理がより簡単かつ効率的になります。

ディープリンク対応

URLベースでの画面遷移が容易になり、外部リンクや通知からの直接遷移が実現できます。
設定のしやすさ、他のDeeplink用のパッケージを導入する必要がないのが、個人的にはかなり良いかなと思いました!

実装例(公式のドキュメントを参考にしています)

実装例
Deeplinkの設定はFlutterのドキュメントがわかりやすく説明しているので、以下リンクを参照してください。

https://docs.flutter.dev/ui/navigation/deep-linking

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.watch(routerProvider);
    return MaterialApp.router(
      routerConfig: router.config(
        
        deepLinkTransformer: (url) async {
          return url;
        },
        deepLinkBuilder: (deepLink) {
          if (deepLink.path.contains('/products/')) {
            return deepLink;
          } else {
            return DeepLink.defaultPath;
            // or DeepLink.path('/')
            // or DeepLink([HomeRoute()])
          }
        },
      ),
    );
  }
}


動作

コード生成でのボイラープレート削減

ルーティング関連のコードを自動生成することで、冗長なコードを減らし、開発効率を高めます。

戻り値の取得

push()pop()で戻り値を受け取れるため、画面遷移後の結果を扱いやすくなります。
実装例

/// pushRouteで遷移する際に以下のようにすることで、maybePopしたあとに値を受け取ることができます。
final result = await context.pushRoute<String>(route);
/// maybePop時に前の画面のスタックに戻る際に、前の画面に渡したい値の型をジェネリクスで渡してあげて、値を挿入することで値を渡すことができます。
await context.maybePop<String>('productName');

ネストされたナビゲーション

ボトムナビゲーションバーやタブバーといったネストされた画面構造にも対応可能で、複雑なナビゲーションもシンプルに実装できます。
ボトムナビゲーションバーやタブバーの実装については、公式のドキュメントか、もしくは以下の記事を参照していただくとわかりやすく説明されていますので、良いかもしれません🙇‍♂️

https://zenn.dev/susatthi/articles/20230427-095829-flutter-auto-route

ルートガードによるリダイレクト

ユーザー認証状態に応じた遷移制御や、パスパラメータやクエリパラメータの値に応じた遷移制御ができるため、安全なルーティングが可能です。

実装例(商品名のクエリパラメータが含まれていないかのガード)

実装例

/// 商品名のチェックを行うガード。
///
/// 商品名のクエリがない場合は商品一覧の画面にリダイレクトする。
class ProductNameCheckGuard extends AutoRouteGuard {
  
  void onNavigation(NavigationResolver resolver, StackRouter router) {
    final queryParams = resolver.route.queryParams;
    final productName = queryParams.optString('productName');

    if (productName?.isEmpty ?? true) {
      resolver.next(false);
      router.replace(const ProductsRoute());
    } else {
      resolver.next(true);
    }
  }
}

トランジションのカスタマイズ

画面遷移時のアニメーションも細かく設定でき、ユーザー体験の向上が期待できます。
AutoRouteのtypeで指定できます。

スケールトランジション

実装例

         type: RouteType.custom(
                transitionsBuilder:
                    (context, animation, secondaryAnimation, child) {
                  return ScaleTransition(
                    scale: Tween<double>(
                      begin: 0.0,
                      end: 1.0,
                    ).animate(
                      CurvedAnimation(
                        parent: animation,
                        curve: Curves.fastOutSlowIn,
                      ),
                    ),
                    child: child,
                  );
                },
              ),


動作

カスタムトランジション(複数のアニメーションを組み合わせ)

実装例

 type: RouteType.custom(
                transitionsBuilder:
                    (context, animation, secondaryAnimation, child) {
                  // フェードと回転を組み合わせる
                  return FadeTransition(
                    opacity: animation,
                    child: RotationTransition(
                      turns: Tween<double>(
                        begin: 0.5,
                        end: 1.0,
                      ).animate(
                        CurvedAnimation(
                          parent: animation,
                          curve: Curves.easeInOut,
                        ),
                      ),
                      child: child,
                    ),
                  );
                },
              ),


動作

実装

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

pubspec.yaml

dependencies:
  auto_route: ^9.2.2

dev_dependencies:
  auto_route_generator: ^9.0.0
  build_runner: ^2.4.13

ルーティングのための画面作成

今回は画面遷移時に値渡しをするケースとしてよくある、親ルートの下に子ルートを持つ階層的なナビゲーション構造を実現させたい場合のケース(商品一覧画面->商品詳細画面)で以下のように実装をしていきます。

  1. ネストされたルーティング構造の実装
    親ルート(商品一覧)にはAutoRouterを継承したProductsRouterPageクラスを作成します。
    このProductsRouterPageが子ルートのコンテナとして機能し、その中で実際の画面遷移が行われます。(実際にドキュメントとして見つけたわけではなく個人的にそのように解釈してます。違いましたら優しく指摘してくださると嬉しいです🙇‍♂️)
  2. パスパラメータや、クエリパラメータ付きの子画面(商品詳細画面)の実装
  3. ルーティングクラスの実装

商品一覧画面

products_page.dart

/// 商品一覧画面のルーティングを管理する。
///
/// products/:idのように階層構造のようになっているパスに遷移する際に必要。
()
class ProductsRouterPage extends AutoRouter {
  /// 商品一覧画面のルーティングを管理する。
  const ProductsRouterPage({super.key});
}

/// 商品一覧画面。
///
/// APIから取得した商品一覧のリストを表示する。
class ProductsPage extends StatelessWidget {
  /// 商品一覧画面
  ///
  /// 商品一覧のリストを表示する。
  /// 商品をタップすると、商品詳細画面に遷移する。
  const ProductsPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Products')),
      body: ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Product $index'),
            onTap: () {
              context.pushRoute(ProductDetailRoute(productId: index));
            },
          );
        },
      ),
    );
  }
}

商品詳細画面

パスパラメータやクエリパラメータを受け取って遷移する実装です。
インスタンス変数を定義したら、名前付きパラメータを定義するのですが、通常の定義の仕方と違い、パスパラメータとして画面に渡したい場合は@PathParam('指定したいパスパラメータの名前')、クエリパラメータとして渡したい場合は@QueryParam('指定したいクエリパラメータの名前')を先頭に指定してください。
詳しく知りたい方は以下のリンクを参照していただくとわかりやすいかと思います。

product_detail_page.dart

/// 商品詳細画面。
///
/// 商品の詳細を表示する。
()
class ProductDetailPage extends StatelessWidget {
  /// 商品詳細画面。
  ///
  /// 商品の詳細を表示する。
  const ProductDetailPage({
    /// 商品の識別id
    ///
    /// @PathParam('productId')アノテーションを付与したパラメータを指定する。
    /// products/[pathParamater]?[queryParamater]=[値]
    ('productId') required this.productId,

    /// 商品の名前
    ///
    /// @QueryParam('productName')アノテーションを付与したパラメータを指定する。
    /// products/[pathParamater]?[queryParamater]=[値]
    ('productName') this.productName,
    super.key,
  });

  /// 商品の識別id
  ///
  /// 商品の識別idを指定する。
  final int productId;

  /// 商品の名前
  ///
  /// 商品の名前を指定する。
  final String? productName;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ProductDetailPage: $productId'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ProductDetailPage: $productIdを取得しています。'),
            Text(productName ?? '商品名はありません'),
          ],
        ),
      ),
    );
  }
}

ルーティング定義のためのルーティングクラス作成

/// [@AutoRouterConfig]アノテーションを付与したクラスがルーティングの設定を行う。
()
/// ルーティングを定義するためのクラス。
///
/// ルーティングの定義は、`@RoutePage`アノテーションを付与したページクラスを`routes`プロパティに設定することで行う。
///
class AppRouter extends RootStackRouter {
  
  List<AutoRoute> get routes => [
        AutoRoute(
          page: ProductsRouterRoute.page,
          path: '/products',
          initial: true,
          children: [
            AutoRoute(
              page: ProductsRoute.page,
              initial: true,
            ),
            AutoRoute(
              page: ProductDetailRoute.page,
              path: ':productId',
            ),
          ],
        ),
      ];
}

コードの自動生成

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

上記コマンドを実行することで、@AutoRouteアノテーションをつけたクラスでルーティング用のコードが生成され、ルーティングクラスでルーティングを定義できるようになります。

生成コマンドの実行のTIPS

@AutoRouteアノテーション付きクラスを作成する毎に自動生成コマンドを実行するのは割と面倒だと感じることがあります。(筆者が面倒なことが嫌いなのでそう感じるだけかもですが。。。😂)
その場合以下の設定をしていることで、少しだけ楽になるかもしれません。(他にも良い方法があれば教えてくださると嬉しいです🙇‍♂️)

Makefileで実行コマンドを設定する。

makeコマンドを使うことで、長くて入力するのも煩わしいコマンドだったり、Aのコマンドを入力後、Bのコマンドを実行したいなどのタスクに対して自動化できるのでオススメです👍
以下設定ファイルです。

Makefile
FLUTTER = flutter

.PHONY: build-runner
build-runner:
	$(FLUTTER) packages pub run build_runner build --delete-conflicting-outputs

上記のように設定をしておくと、Makefileがあるルートでmake build-runnerと実行することで、自動生成コマンドを実行することができます。


VSCodeでtasks.jsonを設定する

tasks.jsonはVSCodeで開発作業を自動化するための設定ファイルです。
.vscode内にtasks.jsonファイルを作成して自動化したい作業を設定していきます。

tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "flutter",
      "command": "flutter",
      "args": [
        "pub",
        "run",
        "build_runner",
        "watch",
        "--delete-conflicting-outputs"
      ],
      "problemMatcher": ["$dart-build_runner"],
      "group": "build",
      "label": "flutter: flutter pub run build_runner watch --delete-conflicting-outputs"
    },
    {
      "type": "flutter",
      "command": "flutter",
      "args": [
        "pub",
        "run",
        "build_runner",
        "build",
        "--delete-conflicting-outputs"
      ],
      "problemMatcher": ["$dart-build_runner"],
      "group": "build",
      "label": "flutter: flutter pub run build_runner build --delete-conflicting-outputs"
    }
  ]
}

上記のように設定をしておくことで、ビルドタスクのショートカットで実行することができます。


ショートカットを押下した状態

動作確認

上記のビルドコマンドを実行した後、実際に画面遷移が正しく行えるかを確認してみましょう。
今回の例では、商品一覧画面から商品詳細画面への遷移がどのように実現されているかをテストします。


GIF画像では、UIに少し手を加えてあります
パスパラメータとクエリパラメータを渡して遷移することができました!

まとめ

本記事では、auto_routeを使ったFlutterアプリのルーティング実装について詳しく解説しました。タイプセーフなルーティングやボイラープレートの削減、ディープリンク対応、トランジションのカスタマイズなど、auto_routeの持つ多彩な機能が、開発の効率化とコードの保守性向上に役立つことをお伝えしました。

これからもFlutterの新しい技術やパッケージをキャッチアップし、実際の開発に役立つ情報を継続的に発信していきたいと考えています。

Discussion