MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

go_router_builderを使って go_routerを使いやすくする

iOS
June 07, 2023

タクシーアプリ『GO』の iOS アプリを開発している井戸田です。

今回はFlutterのgo_routerライブラリをもっと便利に使うことができるgo_router_builderというライブラリについて紹介します。


そもそもgo_routerとは

go_routerはFlutter公式で出している、Navigator2.0を簡単に扱えることができるライブラリです。 URLベースで遷移したい画面を指定することができます。

https://pub.dev/packages/go_router

go_routerの全体のコード

ルーティングの設定部分と遷移部分の全体のコードは下記のようになるかと思います。

ルーティングの設定部分

final goRouter = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      name: 'first',
      path: '/',
      builder: (context, state) => const FirstPage(),
      routes: [
        GoRoute(
          name: 'second',
          path: 'second/:secondId',
          builder: (context, state) => SecondPgae(
            secondId: int.parse(state.pathParameters['secondId']!),
          ),
          routes: [
            GoRoute(
              name: 'third',
              path: 'third',
              builder: (context, state) => ThirdPage(
                secondId: int.parse(state.pathParameters['secondId']!),
                filter: state.queryParameters['filter']!,
              ),
            ),
          ],
        ),
      ],
    ),
  ],
);

遷移部分

// goの場合
onPressed: () => context.go('/second/1/third?filter=test')

// goNamedの場合
onPressed: () => context.goNamed(
  'third',
  pathParameter: <String, String>{
    'secondId': '1'
  },
  queryParameter: <String, String>{
    'filter': 'test'
  }
)

go_routerのつらいところ

go_routerのつらいところの1つとして全てStringで制御しないといけないところだと思います。

1. 自分で定数化する必要がある

name(third)の部分はStringのままでもいいですが定数化したくなると思います。

Before

context.goNamed(
  'third',
  // ...
)

After

class ThirdPage extend StatelessWidget {
  static const routeName = 'third';
}

context.goNamed(
  ThirdPage.routeName,
  // ...
)

2. pathParametersとqueryParametersのKeyがString

'secondId'’filter’ はStringで指定しないといけないためtypoしてても気付きにくいです。

GoRoute(
  name: 'third',
  path: 'third',
  builder: (context, state) => ThirdPage(
    secondId: int.parse(state.pathParameters['secondId']!),
    filter: state.queryParameters['filter'],
  ),
),

context.goNamed(
  'third',
  pathParameters: <String, String>{
    'secondId': '1'
  },
  queryParameters: <String, String>{
    'filter': 'test'
  }
)

3. pathParametersとqueryParametersのvalueはString

pathParametersやqueryParametersのvalueはStringのため、それぞれのページで定義した型に自ら変換する必要があります。

GoRoute(
  name: 'third',
  path: 'third',
  builder: (context, state) => ThirdPage(
    secondId: int.parse(state.pathParameters['secondId']!),
    filter: state.queryParameters['filter'],
  ),
),

それらを解決できるのがgo_router_builderです!

go_router_builderとは

go_routerの拡張ライブラリで、Flutter公式のライブラリです。

go_routerの機能更新に追従してgo_router_builderでもできるようになっているため、基本的にgo_routerでできることはgo_router_builderでもできます。

ファイルを自動生成するためbuild_runnerが必要になります。

https://pub.dev/packages/go_router_builder

go_router_builderの使い方

全体のコード

ルーティング設定部分

part 'go_router_builder.g.dart';

final goRouterBuilder = GoRouter(routes: $appRoutes);

<FirstPageRoute>(
  path: '/',
  routes: [
    TypedGoRoute<SecondPageRoute>(
      path: 'second/:secondId',
      routes: [
        TypedGoRoute<ThirdPageRoute>(path: 'third'),
      ],
    ),
  ],
)
class FirstPageRoute extends GoRouteData {
  const FirstPageRoute();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const FirstPage();
  }
}

// …

class ThirdPageRoute extends GoRouteData {
  ThirdPageRoute({required this.secondId, this.filter});

  final int secondId;
  final String? filter;

  
  Widget build(BuildContext context, GoRouterState state) {
    return ThirdPage(secondId: secondId, filter: filter);
  }
}

遷移部分

onPressed: () => ThirdPageRoute(
  secondId: 1,
  filter: 'test',
).go(context)

ルーティング設定部分

まずは自動生成のファイル名を指定

part 'go_router_builder.g.dart';

GoRouterのグローバル変数を定義。$appRoutesは自動生成されたトップレベルのGoRoute配列のインスタンスです。

part 'go_router_builder.g.dart';

+ final goRouterBuilder = GoRouter(routes: $appRoutes);

GoRouteDataを継承したThirdPageRouteクラスを作成。buildメソッドをオーバーライドし表示したいページを指定します。

go_routerの

  • pathParametersの場合はfinal int secondIdのようにnon nullにします
  • queryParametersの場合は final String? fileterのようにoptionalにします

どちらもString型ではなくて良いです。


class ThirdPageRoute extends GoRouteData {
  ThirdPageRoute({required this.secondId, this.filter});

  final int secondId;
  final String? filter;

  
  Widget build(BuildContext context, GoRouterState state) {
    return ThirdPage(secondId: secondId, filter: filter);
  }
}

トップレベルのrouteには@TypedGoRouteアノテーションをつけます。

ルーティングの階層はgo_routerの時と同じようにroutes引数で行います。routes内ではTypedGoRoute<T>を指定し、Tには作成したSecondPageRouteクラスを指定します。

pathの指定はgo_routerと変わりません。

<FirstPageRoute>(
  path: '/',
  routes: [
    TypedGoRoute<SecondPageRoute>(
      path: 'second/:secondId',
      routes: [
        TypedGoRoute<ThirdPageRoute>(path: 'third'),
      ],
    ),
  ],
)
class FirstPageRoute extends GoRouteData {
  const FirstPageRoute();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const FirstPage();
  }
}

ここまで終わったら$ flutter pub run build_runner build をして、ファイルを自動生成させます。

遷移部分

画面遷移はGoRouteDataを継承したRouteクラスを使用します。

onPressed: () => ThirdPageRoute(
  secondId: 1,
  filter: 'test',
).go(context)

go_routerとgo_router_builderの比較

1. 自分で定数化する必要がない

go_router_builderではルーティング設定部分で定義したRouteクラスを使用するため、自分で定数化する必要がありません。

go_router

class ThirdPage extends StatelessWidget {
  static const routeName = 'third';
}

onPressed: () => context.goNamed(
  ThirdPage.routeName,
  // ...
)

go_router_builder

ThirdPageRoute(
  secondId: 1,
  filter: 'test',
).go(context)

2. pathParametersやqueryParametersの値の設定/取得の際にString型Keyを指定する必要がない

pathParamters、queryParamtersともにMap<String, String>型管理ではなくなるため、String型Keyを指定しなくてよいです。

go_router

context.goNamed(
  'third',
  pathParameter: <String, String>{
    'secondId': '1'
  },
  queryParameter: <String, String>{
    'filter': 'test'
  }
)

GoRoute(
  name: 'third',
  path: 'third',
  builder: (context, state) => ThirdPage(
    secondId: int.parse(state.pathParameters['secondId']!),
    filter: state.queryParameters['filter'],
  ),
),

go_router_builder

ThirdPageRoute(
  secondId: 1,
  filter: 'test',
).go(context)

3. 型変換をする必要がない

go_routerの場合は自身で型変換をする必要がありましたが、go_router_builderを使用することによって、自動生成ファイル内でStringから指定した型に変換してくれるようになります。

go_router

GoRoute(
  name: 'third',
  path: 'third',
  builder: (context, state) => ThirdPage(
    secondId: int.parse(state.pathParameters['secondId']!),
    filter: state.queryParameters['filter'],
  ),
),

go_router_builder

class ThirdPageRoute extends GoRouteData {
  ThirdPageRoute({required this.secondId, this.filter});

  final int secondId;
  final String? filter;

  
  Widget build(BuildContext context, GoRouterState state) {
    return ThirdPage(secondId: secondId, filter: filter);
  }
}

go_router_builderのつらいところ

class ThirdPageRoute extends GoRouteData {
  ThirdPageRoute({required this.secondId, this.filter});

  final int secondId;
  final String? hogeFuga;

  
  Widget build(BuildContext context, GoRouterState state) {
    return ThirdPage(secondId: secondId, filter: hogeFuga);
  }
}

上記のように final String? hogeFugaとqueryParamtersを指定した場合、URLのクエリーパラメータのKeyが hoge-fuga とケバブケースになってしまいます。

An image from Notion

issue化はされているのですが、未だ解決はされていないです。

https://github.com/flutter/flutter/issues/103023

まとめ

go_router_builderを使うことで、go_routerでString型で宣言されていた部分が自由に指定できるようになるのは便利だと感じました。

しかしクエリーパラメータのkeyが2単語以上の場合はケバブケースになってしますので、どうしても嫌な場合は1単語にするなど考慮が必要になってくると思います。


We're Hiring!

📢
GO株式会社ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @goinc_techtalk のフォローもよろしくお願いします!