7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlutterAdvent Calendar 2023

Day 15

go_router_builderを使って、画面遷移を制御する

Last updated at Posted at 2023-12-14

はじめに

go_routerを使うと、Flutterでの画面の遷移を簡潔に制御できます。以下は、go_routerで遷移処理を全て書いた場合の例です。

app_routing.dart
class AppRouteKeys {
  static const home = 'home';
  static const counseling = 'counseling';
  static const counselingPayment = 'counselingPayment';
  static const counselingPaymentComplete = 'counselingPaymentComplete';
  static const inquiry = 'inquiry';
  static const setting = 'setting';
}

final GlobalKey<NavigatorState> rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> homeNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'home');

final routes = [
  ShellRoute(
    navigatorKey: homeNavigatorKey,
    builder: (context, state, child) => RootPage(page: child),
    routes: [
      GoRoute(
          path: '/home',
          name: AppRouteKeys.home,
          builder: (context, state) => const HomePage()),
      GoRoute(
          path: '/inquiry',
          name: AppRouteKeys.inquiry,
          builder: (context, state) => const InquiryPage()),
      GoRoute(
          path: '/setting',
          name: AppRouteKeys.setting,
          builder: (context, state) =>
              SettingPage(initTab: state.uri.queryParameters['tab'])),
    ],
  ),
  GoRoute(
      path: '/counseling/:counselingId',
      name: AppRouteKeys.counseling,
      parentNavigatorKey: rootNavigatorKey,
      builder: (context, state) => CounselingPage(
          counselingId: int.parse(state.pathParameters['counselingId']!)),
      routes: [
        GoRoute(
          path: 'payment',
          name: AppRouteKeys.counselingPayment,
          parentNavigatorKey: rootNavigatorKey,
          builder: (context, state) => PaymentPage(
              counselingId: int.parse(state.pathParameters['counselingId']!)),
          routes: [
            GoRoute(
                path: 'complete',
                name: AppRouteKeys.counselingPaymentComplete,
                parentNavigatorKey: rootNavigatorKey,
                builder: (context, state) => PaymentCompletePage(
                    counselingId:
                        int.parse(state.pathParameters['counselingId']!))),
          ],
        ),
      ]),
];

final router = GoRouter(
  navigatorKey: rootNavigatorKey,
  initialLocation: '/home',
  routes: routes,
);

以上が遷移の定義で実際に呼び出して移動する場合は、以下のように呼び出します。どこに移動したいかは明確ですが、画面に対して、 pathParametersやqueryParametersを渡すことによって、画面間のデータの受け渡しが可能になります。しかし、そのデータがその画面に合っているかどうかは、自分で確認しなければなりません。

context.pushNamed(
  AppRouteKeys.counseling,
  pathParameters: {'counselingId': '1'},
);
context.pushNamed(AppRouteKeys.setting, 
  queryParameters: {'tab': 'counseling'})

これを、go_router_builderを使った形に書き換えると、必要なpathParametersやqueryParametersの定義がわかり、タイプチェックするように書き出されるので、必要な変数を渡すミスを防ぐことができます。実際にどのように変換するのか、この記事で説明したいと思います。

サンプルについて

今回の実装例のサンプルを用意しました。各状態をbranchで置いているので、参考にしてください。

branch 内容
base go_routerのみ
use-go_router_builder go_router_builderに定義の置き換え
add-transition-page 遷移処理と移動処理を書き換え

go_routerの定義を置き換える

先ほどの例で、routesを定義している部分で全体の遷移が決まりますが、この部分をgo_router_builderの定義に置き換えていきます。配列でShellRouteとGoRouteは、TypedShellRouteとTypedGoRouteに置き換えられます。ルートになるTypedGoRouteはアノテーションで定義して、TypedGoRouteで使用するgo_router_builderの遷移設定のクラスを定義します。先ほどの例を書き直すと以下のようになります。

app_routing.dart
part 'app_routing.g.dart';

@TypedShellRoute<RootRoute>(
  routes: [
    TypedGoRoute<HomeRoute>(
      path: '/home',
      name: AppRouteKeys.home,
    ),
    TypedGoRoute<InquiryRoute>(
      path: '/inquiry',
      name: AppRouteKeys.inquiry,
    ),
    TypedGoRoute<SettingRoute>(
      path: '/setting',
      name: AppRouteKeys.setting,
    )
  ],
)
class RootRoute extends ShellRouteData {
  static final GlobalKey<NavigatorState> $navigatorKey = homeNavigatorKey;
  const RootRoute();

  @override
  Widget builder(BuildContext context, GoRouterState state, Widget navigator) {
    return RootPage(page: navigator);
  }
}

@TypedGoRoute<CounselingRoute>(
  path: '/counseling/:counselingId',
  name: AppRouteKeys.counseling,
  routes: [
    TypedGoRoute<PaymentRoute>(
      path: 'payment',
      name: AppRouteKeys.counselingPayment,
    ),
    TypedGoRoute<PaymentCompleteRoute>(
      path: 'complete',
      name: AppRouteKeys.counselingPaymentComplete,
    ),
  ],
)
class CounselingRoute extends GoRouteData {
  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
  final int counselingId;

  const CounselingRoute({
    required this.counselingId,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return CounselingPage(
      counselingId: counselingId,
    );
  }
}

ShellRouteData、GoRouteDataを継承して新たなクラスを定義します。navigatorKeyやparentNavigatorKeyはstaticなプロパティとして定義し、pathParametersやqueryParametersはコンストラクタで渡すように定義します。

app_routing.dart
class PaymentRoute extends GoRouteData {
  final int counselingId; // pathParameters

  const PaymentRoute({
    required this.counselingId,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return PaymentPage(
      counselingId: counselingId,
    );
  }
}

class SettingRoute extends GoRouteData {
  final String? tab; // queryParameters

  const SettingRoute({
    this.tab,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return SettingPage(
      initTab: tab,
    );
  }
}

build_runnerの実行

定義した内容もビルドしないと、利用できません。build_runnerを実行してファイルを生成します。

flutter packages pub run build_runner build

実行すると、app_routing.g.dartが生成され、呼び出しに必要なExtensionやGoRouterに渡す$appRoutesを生成します。

app_routing.dart
final typedRouter = GoRouter(
  navigatorKey: rootNavigatorKey,
  initialLocation: '/home',
  routes: $appRoutes,
);

遷移処理を修正

context.pushNamedを使って遷移処理を記述していましたが、移動を必要なパラメータを忘れずに呼び出して定義できます。

PaymentRoute(counselingId: 1).push(context);
SettingRoute(tab: 'counseling').push(context);

GoRouteDataを別ファイルに定義する

go_router_builderのルートで利用するGoRouteの定義は、アノテーションで定義する必要はありますが、階層下の場合は同じファイルで定義する必要はないので、partで分割したほうが整理しやすいと思います。

image.png

app_routing.dart
part 'counseling.dart';
part 'root.dart';
counseling.dart
part of 'app_routing.dart';

class PaymentRoute extends GoRouteData {
  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
  final int counselingId;

  const PaymentRoute({
    required this.counselingId,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return PaymentPage(
      counselingId: counselingId,
    );
  }
}

class PaymentCompleteRoute extends GoRouteData {
  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
  final int counselingId;

  const PaymentCompleteRoute({
    required this.counselingId,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return PaymentCompletePage(
      counselingId: counselingId,
    );
  }
}

root.dart
part of 'app_routing.dart';

class HomeRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return HomePage();
  }
}

class InquiryRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return InquiryPage();
  }
}

class SettingRoute extends GoRouteData {
  final String? tab;

  const SettingRoute({
    this.tab,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return SettingPage(
      initTab: tab,
    );
  }
}

画面の遷移処理をカスタマイズする

GoRouteDataで遷移に必要な定義のみを書いていましたが、builder以外にbuildPageを使うと、遷移の際のトランジションを個別に定義できます。GoRouteDataに個別に書くと、遷移処理の統一もできないので、別途抽象クラスを定義して、それを利用する形にすれば、トランジションをまとめられます。

以下のようにGoRouteDataを継承したクラスを作成します実際のトランジションは、transitionsBuilderに記述します。

route_data.dart
abstract class BottomGoRouteData extends GoRouteData {
  const BottomGoRouteData();

  @override
  CustomTransitionPage<void> buildPage(
    BuildContext context,
    GoRouterState state,
  ) {
    return CustomTransitionPage<void>(
      // 画面に必要な情報を引き継ぐ
      key: state.pageKey,
      name: state.name ?? state.path,
      arguments: <String, String>{
        ...state.pathParameters,
        ...state.uri.queryParameters,
      },

      // トランジションの構成
      opaque: false,
      transitionDuration: const Duration(milliseconds: 500),
      reverseTransitionDuration: const Duration(milliseconds: 400),
      child: build(context, state),
      transitionsBuilder: (
        context,
        animation,
        secondaryAnimation,
        child,
      ) {
        return SlideTransition(
          position: animation.drive(
            Tween<Offset>(
              begin: const Offset(0, 1),
              end: Offset.zero,
            ).chain(
              CurveTween(
                curve: animation.status == AnimationStatus.reverse
                    ? Curves.easeInCubic
                    : Curves.easeOutCubic,
              ),
            ),
          ),
          child: child,
        );
      },
    );
  }
}

実際に利用する際には、GoRouteDataを使っていた部分をBottomGoRouteDataに置き換えるだけで利用できます。

class CounselingRoute extends BottomGoRouteData {
  static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
  final int counselingId;

  const CounselingRoute({
    required this.counselingId,
  });

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return CounselingPage(
      counselingId: counselingId,
    );
  }
}

まとめ

go_routerだけでも十分利用できますが、遷移時に必要なパラメータを渡す場合は、慎重になる必要がありました。go_router_builderを使うと、必要なパラメータの型や内容を把握できるので、繊維に失敗することがなくなります。go_routerを使う場合は、ぜひ、go_router_builderを利用してください。

最後に、株式会社MG-DXでは自社のサービスとして、医療機関、薬局向けに薬急便のサービスを提供しています。基本はWebでのサービスですが、Flutterで、iOS/Windows向けのアプリを作っています。開発に興味がある方は、是非お問合せください。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?