パッケージ

go_routerはもう時代おくれ?go_router_builderの使い方解説

Flutterでのルーティング設定、煩雑だと感じたことはありませんか?
特にgo_routerを使っていると以下のような問題に直面したことがある人も多いはずです。

課題

  • ページ遷移時に、毎回URLを指定するのは煩雑で誤った挙動を起こす可能性がある
  • ページ遷移時に受け取る引数は常にString型で受け取るから、型安全が保証できない
  • シンプルにルーティングの際のコツがあれば知りたい
プログラマーA

https://flutter-hub.org/1d0

そう!まさにそのような点を改善してくれるのが「go_router_builder」なんだ

まなびと

この記事では、go_router_builderパッケージを使った型安全でシンプルなルーティングの実現方法を解説します。

これにより、コードの可読性と保守性を大幅に向上させ、ページ遷移時のストレスを軽減できます。ルーティングのベストプラクティスを知りたい方は、ぜひ最後までお付き合いください!

以下に、実際にgo_router_builderパッケージを使ったアプリのgithubレポジトリのurlを載せているので気になる方はぜひ覗いてみてください。

ソースコード

最後には、ルーティングの際につかえる裏技も紹介するからぜひ最後まで読んでみてね!

まなびと

実装例

実装で行うことは以下のようになります。

実装工程

Step1: 必要なパッケージをインストール
Step2: 各ルートのクラスと関連する画面を設定
Step3: build_runnerをターミナルで実行することでgo_builder_routerのコードを自動生成
Step4: MaterialAppに生成されたrouterを設定
Step5: ページ遷移を実装

では各工程を簡単に見ていきましょう。

必要なパッケージをインストール

以下のコマンドをプロジェクトのトップディレクトリで実行してください

flutter pub add go_router
flutter pub add dev:go_router_builder
flutter pub add dev:build_runner

これで必要なパッケージはインストールされました。

各ルートのクラスを設定

go_routerとは少し違うスタイルだけど、基本的には各ルートに対してパスとページを設定しているだけだよ

まなびと

次に各ルートのクラスを設定します。
go_routerではpathの中で直接どのパスにどのページが対応するかを設定します。

一方で、go_router_builderではRouteDataというクラスを継承したクラスを各ルートに対して設定します。そのクラスに対して、どの画面をビルドするのかを宣言します。
ほとんどのアプリでは、レイアウト(go_routerではShellRoute)が必要となるのでここでもShellRouteを指定する前提で実装します。

以下のようになります。

@TypedShellRoute<MyShellRoute>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<HomeRoute>(
      path: '/',
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<SettingRoute>(path: 'setting'),
        TypedGoRoute<UsersRoute>(
          path: 'users',
          routes: <TypedRoute<RouteData>>[
            TypedGoRoute<UserRoute>(path: ':id'),
          ],
        ),
      ],
    ),
  ],
)
class MyShellRoute extends ShellRouteData {
  const MyShellRoute();

  static final $navigatorKey = shellNavigatorKey;

  @override
  Widget builder(BuildContext context, GoRouterState state, Widget navigator) =>
      MobileShellRouteScreen(navigator);
}

まず最初に@TypedShellRouteでここにshellrouteのルートを設定しているということを明示します。これはbuild_runnerでコードを自動生成するためのものです。その中にShellRouteとなるルートのクラス名を記載します。

そして、そのクラスがビルドする画面を設定します。navigatorは、ShellRouteのchildとして表示される画面を示します。

その後に実際のShellRouteのクラスをShellRouteDataを継承する形で宣言します。ShellRouteではない普通のルートについてはGoRouteDataクラスを継承させます。

このページではidと名前を引数として受け取ろうとしています。先ほど設定したルートを見ればわかるのですが、このクラス名に一致する部分のpathは/users/:idとなっています。ここからidはパスパラメータとして、nameはクエリパラメータとして受け取られることがわかるのです。

パスパラメータもクエリパラメータも、遷移先の引数では同じように扱われるんだ
URLにパスパラメータ(:idなど)を含んでいる場合はその引数をパスパラメータとしてURLから取得する、それ以外はパスパラメータからという感じだ

まなびと

用語確認

パスパラメータ:パスの中で「/users/:id」という形式で記載され、idの部分に具体的な値が入ります。これは特定の値を指定するために使われます。

クエリパラメータ:パスの中で「/users?age=25」のように?の後に値のkey-valueという形で表現されます。これはフィルタリングをしたりオプションを指定する際に使用されます。

後ほど紹介しますが、値を渡すときはURLに直接値を入れるのではなく引数として値を渡すので型安全が保証されるのです。

以下のような感じです。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import '../../../../../app.dart';
import '../../../../../extension/extension_buildcontext.dart';
import 'user/desktop/desktop_user_screen.dart';
import 'user/mobile/mobile_user_screen.dart';

class UserRoute extends GoRouteData {
  const UserRoute(this.name, {required this.id});
  final String? name;
  final int id;
  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;

  @override
  Page<void> buildPage(BuildContext context, GoRouterState state) =>
      context.isDesktop
          ? DesktopUserScreen(id, name, key: state.pageKey)
          : MobileUserScreen(id, key: state.pageKey);
}

※実際のコードではlib/go_router_builder/route/router.dartと各画面のディレクトリの〇〇_route.dartに記載しています。

これでルートの設定は完了しました。

コードの自動生成

次に、build_runnerをターミナルで実行することでgo_builder_routerのコードを自動生成していきます。

単純に以下のコマンドをターミナルで実行するだけです。ちなみに、-dフラグはもし生成するはずのファイルがすでに存在している場合はそれを削除して新しくファイルを生成するということを意味しています。

flutter pub run build_runner build -d

開発の際に気をつけて欲しいのは、ルートに変更があった場合に毎回このコマンドを実行しなければならないということです。以下のコマンドを実行すれば、ファイルの変更を監視して自動でコードを生成してくれます。

flutter pub run build_runner watch

また、発展の章で紹介するのですが、私はMakefileにコマンドをあらかじめ記載しておき必要に応じて短いコマンドで実行させるという方法を好んで使っています。

自動生成のおかげで、型安全のチェックとurlを使わないページ遷移ができるのはとてもありがたいね

まなびと

MaterialAppに生成されたrouterを設定

go_routerと同じようにMaterialAppにルートを設定します。その変数名は固定で$appRoutesという名前になります。

ソースコード

Widget build(BuildContext context) {
    final router = GoRouter(
      routes: $appRoutes,
      initialLocation: '/',
      navigatorKey: rootNavigatorKey,
      errorBuilder: (context, state) {
        return ErrorRoute(state.error.toString()).build(context, state);
      },
    );
    return MaterialApp.router(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      routerConfig: router,
    );
  }

ページ遷移の実装

最後にページ遷移の実装部分を紹介します。

通常のgo_routerでは、ページ遷移の際に直接値をURLに入れてそのURLに遷移するということをします。これによって値は一旦String型に変換されるので、遷移先で使用する際に型安全が保証されていない形になります。

一方で、go_routerを使うと値はその値のまま渡されることになります。これによって受け取る際は型が保証されているので、非常に使い勝手が良いのです。

また、もう一つgo_router_builderのメリットがあります。それはページ遷移の際にURLを打ち込む必要がないということです。遷移したいページに関連するクラスを通して遷移するためのメソッド(goなど)を呼び出せば良いだけなのです。これによって意図しないURLへ遷移してしまうことを防ぐことができます。

import 'package:flutter/material.dart';

import '../../../../../../route/router.dart';
import '../../sub_sreens/user_route.dart';

class MobileUsersListTile extends StatelessWidget {
  const MobileUsersListTile(this.index, {super.key});
  final int index;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: const Icon(Icons.person),
      title: InkWell(
        onTap: () {
          UserRoute(null, id: index).go(context);
        },
        child: Text('User $index'),
      ),
    );
  }
}
プログラマーA

go_routerでは、urlを書き込まないといけないし値を直接urlに入れ込まないといかなかったから危険性を孕んでると思ってたんだよね

けどgo_router_builderだとコードもシンプルで上の問題も解決されてていいね!

そうだね、とてもシンプルな記述に加えてgo_routerの問題を解決できているのは最高だね!

まなびと

実際に動かしてみる

コードは以下のように動きます。

見かけ上はgo_routerと変わりありません。ただし、開発上ではかなりの違いがあります。

  • 引数の型安全が保証されている
  • ページ遷移の煩雑さの解消

これらのメリットを享受できるgo_router_builderは導入する価値ありだと思います!

ここからは、基本的な実装には不要ですがあればUI・UXのレベル向上につながると思うのでぜひ試していみてください。

裏技

ここからはUXと開発の効率を上げるための裏技を紹介していくよ!
go_router_builder以外でも使えるものもあるから是非チャレンジしてみてほしい

まなびと

リアクティブUIのための実装

スマホの登場以来、フロントエンドの常識は書きかわりました!

なぜなら、PCとスマホの両方の画面サイズに対応しなければUXのレベルが急降下してしまうからです。そのようなUIを「リアクティブUI」と呼びます。ここではリアクティブUIのためのちょっとしたテクニックを紹介したいと思います。

まず画面サイズをどうやって取得するのかおさらいしましょう。下のようにcontextから取得するのが一般的です。

MediaQuery.of(this).size.width;  //画面の幅
MediaQuery.of(this).size.height;  //画面の高さ

しかしこれを毎回画面選択する際に呼び出して、例えば600pxより小さいならモバイルの、それ以外はではPC用の画面を提供とするのは煩雑です。そこでcontext自体に画面の大きさを測り、それがモバイルサイズなのかPCサイズなのかを判定する機能を追加するという方法を使ってみましょう!

ここで出てくるのがextension / onというキーワードです。これはあるクラスに対して、機能を追加するときに使われます。以下のように実装します。

ソースコード

extension ResponsiveExtension on BuildContext {
  static double threshold = 600;
  bool get isMobile => MediaQuery.of(this).size.width < threshold;
  bool get isDesktop => MediaQuery.of(this).size.width >= threshold;
}

これで、contextが使える場所ではどこでもモバイルかPCか判断できます。go_router_builderでは以下のようにどちらの画面を返すのかを、
GoRouteDataを継承したクラスを記述する際に用います。

https://github.com/honganji/Flutter-Hub/blob/main/lib/go_router_builder/ui/shell_route/sub_screens/home/home_route.dart

class HomeRoute extends GoRouteData {
  const HomeRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) =>
      context.isDesktop ? const DesktopHomeScreen() : const MobileHomeScreen();
}

Makefileの作成

Makefileを使えば、長ったらしいコマンドも1行の短いコマンドで一気に実行できるよ!

まなびと

Makefileという名前に聞き覚えがない方も多いかもしれません。このファイルは、もともとある作成したいファイルのためにいくつかのコマンドを一気に実行するためのものです。

一方で、実はファイルを作成しなくてもただコマンドを呼び出すのに少ないコマンドで済ませるという目的にも使えるのです。例として、
Flutter-HubのMakefileの一部を紹介します。

ソースコード

こちらは、自動生成コードを作成するためのコマンドです。先ほど紹介したように、自動生成のためのコードに変更があった場合は毎回このコマンドを呼ぶ必要があります。その度にdart run build_runner buildを実行するのは面倒ですよね。

そこでこの記述をMakefileに記述しておくと、以下のコマンドだけで実行したいコマンドを実行することができるのです。

make gen

makeの後に、ターゲットとなる名前を呼び出せば良いのです。ここで注意なのですが、Makefileの最後に以下の記述を載せることを忘れないでください。

.PHONY: gen

ターゲットとなる名前は、実はファイルの名前を指しています。なのでもし「gen」という名前のファイルやディレクトリが存在した場合は、コマンドが実行されないことになります。

この記述を載せていれば、たとえそのような場合でもコマンドは正しく実行されることにもなるのです。

Errorページの追加

errorページの表示は必須だよ。エラーが起きないことが最高だけど、エラーは起きるものという前提で実装するのがいいよ
これはプログラミング全般に言えることだね

まなびと

最後にエラーページの追加の仕方を紹介しようと思います。

アプリ開発でエラー処理はとても重要なものです。予測していない動作が発生した場合にアプリが停止してしまっては、ユーザーはもうそのアプリを使いたいと思わなくなってしまうでしょう。

そこできちんとデザインされたエラーページの提供が重要となるのです。設定方法は簡単です。まず以下のようにError用のクラスを宣言して、そのクラスに関連するページを返すようにします。

ソースコード

class ErrorRoute extends GoRouteData {
  const ErrorRoute(this.message);
  final String message;

  @override
  Widget build(BuildContext context, GoRouterState state) => context.isDesktop
      ? DesktopErrorScreen(message)
      : MobileErrorScreen(message);
}

そして、MaterialAppに登録するrouterにこのクラスを設定するだけです。

ここでは、エラーメッセージを受け取りそれを表示するように設定しています。

ソースコード

final router = GoRouter(
      routes: $appRoutes,
      initialLocation: '/',
      navigatorKey: rootNavigatorKey,
      errorBuilder: (context, state) {
        return ErrorRoute(state.error.toString()).build(context, state);
      },
    );

まとめ

go_router_builderを使いこなせるようになれば、開発スピードも上がるし開発の質(安全性)も上がるはずだよ!

まなびと

いかがだったでしょうか?これまでgo_routerを使ってルーティングを設定していた方もそうでない方も、go_router_builderの使い勝手の良さに気づいてもらえたと思います。

以下の二点がこのパッケージの推しポイントです。

  • ページ遷移時の引数の型安全性を保証
  • ページ遷移の手間と誤作動が減る

go_routerと比べると少し書かなければならないコードは増えますが、上記の二点を鑑みると実装する価値は十分にあると思います。
ぜひ試してみてください!

小さなことこそが違いを生みます!毎日の学びを大切にしていきましょう!
それではみなさん、さようなら〜

-パッケージ
-, , ,