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

## はじめに

[go_router](https://pub.dev/packages/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を渡すことによって、画面間のデータの受け渡しが可能になります。しかし、そのデータがその画面に合っているかどうかは、自分で確認しなければなりません。

```dart
context.pushNamed(
  AppRouteKeys.counseling,
  pathParameters: {'counselingId': '1'},
);
```

```dart
context.pushNamed(AppRouteKeys.setting, 
  queryParameters: {'tab': 'counseling'})
```

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

### サンプルについて

今回の実装例の[サンプル](https://github.com/ueki-tomohiro/go_router_builder_sample)を用意しました。各状態を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を実行してファイルを生成します。

```shell
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を使って遷移処理を記述していましたが、移動を必要なパラメータを忘れずに呼び出して定義できます。

```dart
PaymentRoute(counselingId: 1).push(context);
```

```dart
SettingRoute(tab: 'counseling').push(context);
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528194/2c54fe43-24d6-5e46-4d9b-676d9dd6e3ee.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に置き換えるだけで利用できます。

```dart
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](https://pub.dev/packages/go_router)だけでも十分利用できますが、遷移時に必要なパラメータを渡す場合は、慎重になる必要がありました。[go_router_builder](https://pub.dev/packages/go_router_builder)を使うと、必要なパラメータの型や内容を把握できるので、繊維に失敗することがなくなります。[go_router](https://pub.dev/packages/go_router)を使う場合は、ぜひ、[go_router_builder](https://pub.dev/packages/go_router_builder)を利用してください。

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