タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

FlutterのRoutingを型安全に扱いたい!!

Sponsored link

標準Routerのつらみ

Flutter標準のRouterの仕組みと書き方はだいたいこんな感じです。

flutter.dev

はい。つらいですね。
具体的にいうと

  • 定義が単純な Map<String, WidgetBuilder>

    • なので補完が効かない
    • なのでタイポで全てが終わる
    • なので後からrouterのkeyを変更するのが大変
  • 引数の渡し方がcontext越しのdynamicでヤバい

    • 普通に渡し忘れる
    • 普通に引き取り忘れる
    • 引数の型を変えてもコンパイルエラーが教えてくれない

などなど、画面が3つくらいしかないTODOアプリなら別にこれでいいとおもいますが、画面が10とか20とかを越えてくるあたりからもうちょい安全なソリューションが欲しくなるとおもいます。
じゃあ簡単なラッパーを書きましょう。

オレRoute

gist.github.com

はい書きました。
ちなみに toMaterialRoutes 内部の Map.fromIterable をfor式を使った形式に書き換えるとbuilderのランタイム型がdynamicで上書きされてしまいランタイムの型エラーが出ます。このクソみたいなバグで大量の時間を無駄にしました。Dartは静的型チェックはクッソガバガバなのにランタイムの型チェックが変な挙動で取り締まってくるから怖いよ〜〜〜

はい準備します。

class _Routes extends RouteSetting {
  final root = Route<void>(initial: true, name: '/', builder: (context, _) => const RootScreen());
  final todos = Route<void>(name: '/todos', builder: (context, _) => const Todos());
  final todo = Route<TodoId>(name: '/todo', builder: (context, id) => const TodoDetail(id));
}
final smartRoutes = Router(_Routes());

// main.dart
      child: MaterialApp(
        title: 'router test',
        routes: smartRoutes.toMaterialRoutes(),

これで仕込みは完了です。
はいじゃあ使ってみましょう

GestureDetector(
  onTap: () => smartRoutes.push(ctx, (s) => s.todo, arguments: todo.id),  // <= idがなかったり型が違ったりするとエラーが出る。やさしいね!

うわー便利!
/todos のような引数のないrouteの呼び出しにはpushWithoutArgumentを使います。こいつに引数ありrouteを渡してもコンパイルエラーが出てくれないので注意。これはDartの型システムな制約があるんでハァ〜〜〜〜しかたねえなって感じです。

もうちょい気が向いてブラシュアップしたらDartパッケージ化するかもしれない。やりかたしらんけど。

こちらからは以上です。

追記

ヘーシャではStoreに使う構造体のIDはDBで使っているリテラルをそのまま使うのではなくラッパークラスを経由して使うことを強く推奨しています。
つまりサーバーサイドではTODOのidの定義がUUID型*1だったとしても

class TodoId extends StoreId<String> {
  const TodoId(String id): super(id);
}

とローカルでは定義するわけですね。
いちいちめんどくせえよと思う向きもあるとおもいますが、規模が大きくなるほどメリットもあるので試してもいいんじゃない?

StoreIdの定義の中身は感じてくれ。

関連:

hachibeechan.hateblo.jp

相変わらずDartもFlutterも何一つ好きにはなれないけど、それはそうとしてアーキテクチャのベストプラクティスも結構知見が溜まってきたので需要ありそうならまとめるかも。

*1:Postgres前提