【Flutter】go_router をタイプセーフに使う方法【go_router_builder】
はじめに
Navigator 2.0 のデファクトスタンダードになっている go_router をより安全に利用できるパッケージ go_router_builder を紹介します!現在 go_router を使っていて、タイプセーフに書けないとお悩みの方にオススメです!
タイプセーフに画面遷移できる
go_router_builder 導入のモチベーションは、タイプセーフに画面遷移できることです。導入前後のコードを見比べてみてもらうのがわかりやすいと思います。
context.go(
'/family/${family.id}/person/${person.id}/details?details=${entry.key.name}',
extra: ++_extraClickCount,
);
// goNamed() の場合
context.goNamed(
PersonDetailsPage.name,
params: {
'fid': family.id,
'pid': person.id.toString(),
},
queryParams: {
'details': entry.key.name,
},
extra: ++_extraClickCount,
);
PersonDetailsRoute(
family.id,
person.id,
details: entry.key.name,
$extra: ++_extraClickCount,
).go(context);
導入前はパスやパラメータ名 ( 例:fid
) を文字列で扱っているため、タイポの危険がつきまといますが、導入後は名前付き引数で扱えるのでタイポを未然に防ぐことが出来ます!あと、値は String
以外の型も扱えるようになります!
それでは、go_router_builder の導入方法を見ていきましょう!
導入方法
環境
Flutter 3.0.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f1875d570e (3 weeks ago) • 2022-07-13 11:24:16 -0700
Engine • revision e85ea0e79c
Tools • Dart 2.17.6 • DevTools 2.12.2
pubspec.yaml を編集する
次のようにパッケージを追加します。build_runner はコードを自動生成するのに使います。
dependencies:
flutter:
sdk: flutter
go_router: ^4.2.2
dev_dependencies:
+ build_runner: ^2.2.0
flutter_test:
sdk: flutter
+ go_router_builder: ^1.0.7
自動生成コードのファイル名を追記する
GoRouter
クラスを生成しているファイルの import 文の次の行に、次の 1 行を追記します。ここでは router.dart
というファイル名を例にとって説明します。
+ part 'router.g.dart';
ルートを定義する
go_router では次のように GoRouter
クラスの routes
プロパティにルートを定義していました。
GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
],
・・・
);
go_router_builder では、次のように @TypedGoRoute
アノテーションと GoRouteDate
クラスを使ってルートを定義し直します。
<HomeRoute>(
path: '/',
)
class HomeRoute extends GoRouteData {
const HomeRoute();
Widget build(BuildContext context) => const HomePage();
}
コードジェネレーターを実行する
次のコマンドを実行して自動生成ファイル ( 例: router.g.dart
) を生成します。
flutter pub run build_runner build --delete-conflicting-outputs
GoRouter
の初期化
自動生成ファイルができたら、GoRouter
クラスの routes
プロパティを次のように書き換えます。これでルートの定義は完了です!
GoRouter(
- routes: [
- GoRoute(
- path: '/',
- builder: (context, state) => const HomeScreen(),
- ),
- ],
+ routes: $appRoutes,
・・・
);
画面遷移
最後に、画面遷移の実装を次のように書き換えます。go_router では次のように画面遷移をしていました。
context.go('/');
// goNamed() の場合
context.goNamed(HomePage.name);
go_router_builder では、次のように GoRouteData
クラスを継承したクラスを使って画面遷移を実装します。
const HomeRoute().go(context);
パスパラメータ、クエリ、Extra の受け取り方法
go_router の時は次のように受け取っていました。
GoRoute(
path: 'details/:details',
builder: (context, state) => PersonDetailsPage(
fid: state.params['fid']!,
pid: int.parse(state.params['pid']!),
details: PersonDetails.valueOf(state.params['details']),
extra: state.extra as int?,
),
);
go_router_builder でどのように受け取るのかを紹介します!
パスパラメータ
例えば、/family/:fid
の fid
を受け取りたい場合は、名前無し、必須パラメータ にします。パスパラメータの型には、String
、int
、double
、bool
、Enum
が指定できました(独自クラスは指定できませんでした)。
class FamilyRoute extends GoRouteData {
const FamilyRoute(
this.fid,
);
final String fid;
Widget build(BuildContext context) => FamilyPage(fid: fid);
}
FamilyRoute(family.id).go(context);
クエリ
例えば、/login?from=aaaaa
の aaaaa
を受け取りたい場合は、名前付き、任意パラメータ にします。パスパラメータ同様にString
、int
、double
、bool
、Enum
が指定できました。
<LoginRoute>(
path: '/login',
)
class LoginRoute extends GoRouteData {
const LoginRoute({
this.from,
});
final String? from;
Widget build(BuildContext context) => LoginPage(from: from);
}
LoginRoute(from: state.subloc).go(context);
Extra
Extra
を受け取りたい場合は、$extra
という特別なパラメータ名の 名前付き、任意パラメータ にします。
class PersonDetailsRoute extends GoRouteData {
const PersonDetailsRoute(
this.fid,
this.pid, {
this.details,
this.$extra,
});
final String fid;
final int pid;
final String? details;
final int? $extra;
Page<void> buildPage(BuildContext context) {
return MaterialPage<Object>(
fullscreenDialog: true,
child: PersonDetailsPage(
fid: fid,
pid: pid,
details: PersonDetails.valueOf(details),
extra: $extra,
),
);
}
}
PersonDetailsRoute(
family.id,
person.id,
details: entry.key.name,
$extra: ++_extraClickCount,
).go(context);
エラー画面、リダイレクト、トランジション
エラー画面、リダイレクト、トランジションなどの実装方法は README に書かれています。是非一読してみてください。
下記の公式サンプルコードもとても参考になりました。
はまりポイント
複数の最上位ルートが自動生成されない場合
次のように複数の最上位ルートを定義するとき、@TypedGoRoute
の定義のすぐ次に GoRouteData
の定義を書かないと、 build_runner を実行したときに正しく自動生成されません。
<HomeRoute>(
path: '/',
)
<LoginRoute>(
path: '/login',
)
class HomeRoute extends GoRouteData {
const HomeRoute();
Widget build(BuildContext context) => const HomePage();
}
class LoginRoute extends GoRouteData {
const LoginRoute({
this.from,
});
final String? from;
Widget build(BuildContext context) => LoginPage(from: from);
}
<HomeRoute>(
path: '/',
)
class HomeRoute extends GoRouteData {
const HomeRoute();
Widget build(BuildContext context) => const HomePage();
}
<LoginRoute>(
path: '/login',
)
class LoginRoute extends GoRouteData {
const LoginRoute({
this.from,
});
final String? from;
Widget build(BuildContext context) => LoginPage(from: from);
}
まだまだ不安定
go_router_builder の Open Issues は 13 件あります(2022/08/02現在)。
特に次の Issue は、Extra を使うとその配下の画面を表示するとエラーが起こるという正常系バグです。go_router および go_router_builder はまだまだ発展途上のパッケージであり、このような正常系バグがまだまだ残っているフェーズであると認識した上で利用する必要がありそうです。
サンプルコードを公開しています
公式のサンプルコード をベースに go_router_builder を使ったサンプルコードを作ったので公開します。
最後に
Flutter 大学という Flutter エンジニアに特化した学習コミュニティに所属しています。オンラインでわいわい議論したり、Flutter の最新情報をゲットしたりできます!ぜひ Flutter 界隈を盛り上げていきましょう!
Discussion