Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

go_routerでの画面遷移をタイプセーフに行いたい

サービス開発パートナー事業部 アプリエンジニアチーム 自称Flutterエンジニアの務台です。最近ジムに通い始めました。弊社はフルリモート勤務なので意識して運動していきたいところです。

Flutter開発において、画面遷移にはgo_routerを使うのが主流かと思います。

かくいう私もgo_routerをよく使っていたのですが、

  • パスパラメータに文字列型しか使えない
  • extraで渡した値がObject?になってしまう

のがかなり使いづらいなぁと感じていました。

そこで今更ではありますが、その辺りを解決してくれるgo_router_builderを使ってみました。

バージョン

  go_router: 6.5.7
  build_runner: 2.2.1
  build_verify: 3.1.0
  go_router_builder: ^1.2.1

つかいかた

パッケージのインポート

まずはpubspec.yamlに使用するパッケージを追加して

dependencies:
  go_router: ^6.5.7

dev_dependencies:
  build_runner: ^2.2.1
  build_verify: ^3.1.0
  go_router_builder: ^1.2.1

パッケージをインポートします。

flutter pub get

ルーティングの定義

まずはルーティングを定義するためのファイルを作成します。 go_router_builderでは自動生成されるファイルを使用するので、part宣言もしておきます。

app_route.dart

part 'app_route.g.dart';

続いてページの宣言を追記していきます。

この時、ページに引数が必要な場合はルートクラスの引数として渡してあげることで、実際の遷移を実装する際にタイプセーフにすることができるようになります。

app_route.dart

// 引数が不要な遷移
@immutable
class HomeScreenRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const HomeScreenView();
  }
}

// 引数が不要な遷移
@immutable
class UserListRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const UserListView();
  }
}

// 引数が必要な遷移
@immutable
class UserDetailRoute extends GoRouteData {
  const UserDetailRoute({
    required this.id,
  });

  final int id;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return UserDetailView(userId: id);
  }
}

また、extraを使用する場合は指定の$extraという変数名で宣言する必要があります。

// extraとして引数を渡す遷移
@immutable
class UserDetailRoute extends GoRouteData {
  const UserDetailRoute({
    this.$extra,
  });

  final UserDetailRoutingParam? $extra;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return UserDetailView(userParam: $extra!);
  }
}

$extraを使うことで任意の値を渡すことができるようになりますが、null許容型にはなってしまうため、個人的にはできるだけ使わない方が良いのかなと思いました。

続いて同ファイルにルーティングを追記します。

ここで先ほど作成したルート宣言とパスを紐付けます。 通常のgo_routerと同様パスパラメータも使用することができ、ルート宣言したクラスの引数として渡した値が自動で置換されてパスパラメータとして使用されます。

app_route.dart

@TypedGoRoute<HomeScreenRoute>(
  path: '/',
  routes: [
    TypedGoRoute<UserListRoute>(
      path: 'user',
      routes: [
        TypedGoRoute<UserDetailRoute>(path: ':id/detail'), // [:id]の部分が引数のidの値に置換される
      ],
    ),
  ],
)

その後自動生成ファイルを作成します。

flutter pub run build_runner build

最後に設定したルーティング情報をGoRouterへ渡します。この時渡す参照は自分で作成したものでなく、自動生成された$appRoutesを使用します。

app_router.dart

class AppRouter extends StatelessWidget {
  AppRouter({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }

  final GoRouter _router = GoRouter(routes: $appRoutes);
}

ルーティングの設定は以上です。

画面遷移

前節で定義したルーティング情報を使ってタイプセーフに画面遷移を行います。

といっても特段難しいことはなく、ルート宣言で定義したクラスをインスタンス化し、gopush等で遷移すればOKです。

// UserListViewへ遷移
onPressed: () =>  const UserListRoute().go(context)

// UserDetailViewへ遷移
onPressed: () => const UserDetailRoute(id: 100).push(context)

// extraを使用してUsreDetailViewへ遷移
onPressed: () => const UserDetailRoute(
  $extra: UserDetailRoutingParam(
    id: 200,
    name: 'ユーザ名',
  ),
).pushReplacement(context)

これでgo_builder_routerを利用して、タイプセーフな画面遷移を行うことができるようになりました!

まとめ

go_router_builderを使用していないと、以下のような遷移を実装する必要があります。

// UserListViewへ遷移
onPressed: () => context.go('/user')

// UserDetailViewへ遷移
onPressed: () => context.go('/user/100/detail')

// extraを使用してUsreDetailViewへ遷移
onPressed: () => context.pushReplacement(
  extra: const UserDetailRoutingParam(
    id: 200,
    name: 'ユーザ名',
  )
)

ページへの遷移を都度ハードコードする必要があったりするので、

  • ページへのパス合ってるんだっけ…
  • 渡す引数あってるんだっけ…

といった実行時エラー系の心配をする必要がなくなります。

まだgo_router_builderを使用していない場合や、新しくアプリを作る時にはドンドン導入していきましょう!

全体像

最後に今回作成したルーティング用のファイルの全体像を掲載します。

app_route.dart

///
/// importは省略
///

part 'app_route.g.dart';

@TypedGoRoute<HomeScreenRoute>(
  path: '/',
  routes: [
    TypedGoRoute<UserListRoute>(
      path: 'user',
      routes: [
        TypedGoRoute<UserDetailRoute>(path: ':id/detail'), // [:id]の部分が引数のidの値に置換される
      ],
    ),
  ],
)

// 引数が不要な遷移
@immutable
class HomeScreenRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const HomeScreenView();
  }
}

// 引数が不要な遷移
@immutable
class UserListRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const UserListView();
  }
}

// 引数が必要な遷移
@immutable
class UserDetailRoute extends GoRouteData {
  const UserDetailRoute({
    required this.id,
  });

  final int id;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return UserDetailView(userId: id);
  }
}

// // extraとして引数を渡す遷移
// @immutable
// class UserDetailRoute extends GoRouteData {
//   const UserDetailRoute({
//     this.$extra,
//   });

//   final UserDetailRoutingParam? $extra;

//   @override
//   Widget build(BuildContext context, GoRouterState state) {
//     return UserDetailView(userParam: $extra!);
//   }
// }

app_router.dart

///
/// importは省略
///

class AppRouter extends StatelessWidget {
  AppRouter({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }

  final GoRouter _router = GoRouter(routes: $appRoutes);
}

FeatureFirstなディレクトリ構成に入門してみた

サービス開発パートナー事業部 アプリエンジニアチーム 自称Flutterエンジニアの務台です。 弊社が加入している関東ITS健康保険組合では、毎年恒例のディズニーチケット割引券の申し込みがスタートしました。被保険者、被扶養者であれば3000円引きでチケットが買えるので、興味のある方は申し込みをお忘れなく!

私が今まで経験したプロジェクトでは全てLayerFirstディレクトリ構成が採用されており、体感では同様のディレクトリ構成を使用している場合が多いのではないかと思います。 しかし、以下の記事で紹介したTodoアプリの作成にあたってはFeatureFirstディレクトリ構成を使ってみました。 techblog.glpgs.com

普段はLayerFirstなディレクトリ構成のプロジェクトを触ることが多い私が、FeatureFirstなディレクトリ構成を採用してみて感じたことを共有できればと思います。

また、以前作成したTodoアプリのリポジトリこちらです。 github.com

ディレクトリ構成

まずはそもそもLayerFirstとFeatureFirstとは何か、についてです。

LayerFirst

LayerFirstなディレクトリ構成とは、そのプロジェクトで採用しているアーキテクチャのレイヤに沿ってディレクトリを分割するという方法です。 Todoアプリの依存関係は以下のようになっています。

---
title: TodoApp Dependencies
---
flowchart TB
    subgraph Presentation
        Widget
        Notifier
        Widget --> Notifier
    end
    subgraph Domain
        Usecase
        ValueObject
        Usecase --> ValueObject
    end
    subgraph Repository
        TodoRepository
    end
    subgraph Data
        LocalData
    end
    Presentation --> Domain
    Repository --> Domain
    Repository --> Data

Todoアプリには4つの階層が存在しているので、それに沿ってディレクトリを作成すると以下のようになります。

lib
  |--data
  |    └--local_data
  |--domain
  |    |--usecase
  |    └--value_object
  |--presentation
  |    |--common_widget
  |    |--todo_create
  |    |    |--todo_create_screen.dart
  |    |    └--todo_create_notifier.dart
  |    |--todo_edit
  |    └--todo_list
  └--repository
      └--todo_repository.dart

このように、どの層に属するかに着目したディレクトリ構成を、LayerFirstなディレクトリ構成といいます。

FeatureFirst

FeatureFirstなディレクトリ構成とは、アプリ内での機能毎にディレクトリを分割するという方法です。 Todoアプリにおけるユースケースを考えると以下のものが挙げられます。

  • Todoを閲覧する
  • Todoを追加する
  • Todoを削除する
  • Todoを編集する

これを大まかにTodoの管理と捉え、アプリとして動作させるために必要な機能を加えると、 Todoアプリには以下のような機能が必要になりそうです。

  • Todoの管理
  • ルーティング
  • (ローカル)データの管理

これを踏まえて、必要なレイヤー等必要なものを追加すると以下のような構成になります。

lib
  └--feature
      |--local_data
      |    └--local_database.dart
      |--router
      └--todo
          |--domain
          |    |--usecase
          |    └--value_object
          |--presentation
          |    |--todo_create
          |    |    |--create_todo_notifier.dart
          |    |    └--todo_create_screen.dart
          |    |--todo_edit
          |    |--todo_list
          |    └--common_widget
          └--repository
              └--todo_repository.dart

このように、アプリの機能に着目してディレクトリを分割する方法を、FeatureFirstなディレクトリ構成といいます。

FeatureFirstにしてみた感想

これを採用してみての感想は、LayerFirstよりも保守しやすくなりそう!ということです。

なりそう!なので実際にどうなのかは分からないのですが、 保守の面で見ると以下の点が、LayerFirstより優れているのではと思います。

  • システムの改修は機能単位で来ることが多いため、どこに手をつければいいか分かりやすい
  • 修正すべきファイルが同じディレクトリにまとまっているため、修正漏れが起きにくい
  • 機能追加時は新しくディレクトリを作れば良いため、あまり影響範囲を意識しなくて良い

またFeatureFirstを使ってみて難しかった点として、オブジェクトが使用されるスコープを意識する必要があるというのがありました。

例えば、LayerFirstではdomainディレクトリにデータオブジェクトを入れておき、それをpresentationディレクトリから使用する、となると思います。 FeatureFirstでは、各機能毎にdomainpresentationディレクトリが存在するので、それぞれで必要なデータオブジェクトだけを、適切な場所に置く必要があります。

このように新しくクラスを作成する際はどこから使用されるか、を意識する必要があります。しかしこれによって関心の分離を意識せざるを得なくなるため、良い点でもあるのかなぁと思います。

まとめ

作成したTodoアプリにFeatureFirstなディレクトリ構成を採用してみました。 正直なところ、この規模のアプリではまだFeatureFirstなディレクトリ構成の恩恵を得ることはあまりできていませんが、アプリの規模が大きくなるにつれてありがたみが見えてくると思います。

ただ、ファイルの置き場所の判断には慣れが必要そうなので、今後もチャンスがあればFeatureFirstなディレクトリ構成を使っていきたいと思いました。

Deno はいいぞ

サービス開発パートナー事業部*1 Android エンジニアの松下です。今年に入ってから利き手の逆でご飯を食べるようにしています。
自分は右利きなので左手で食べています。これで右脳が鍛えられるとか…?まだあまり実感はないですが。

皆さんは雑なスクリプトを書くときは何を使いますか? RubyPython などでも良いですが、外部ライブラリを使う際はちょっと面倒だったりします。
そこで Deno !試してみませんか!一回だけ!一回だけだからさ…。

*1:AIR Design for Apps 事業部が改名しました

続きを読む