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);
}