🌊

Flutter go_router_builder v3.0.0 アップデート奮闘記

に公開

こんにちは、URBAN HACKSでAndroid/Flutterエンジニアをしている後藤です。

はじめに

Flutterのルーティングを強力にサポートするgo_routerと、そのコード生成ツールgo_router_builder
今回は、go_router_builderをv3.0.0にアップデートした際の体験をまとめます。

go_router_builder v3.0.0の主な変更点

  • 公式リリースノート: CHANGELOG
  • 主な変更点
    • v3.0.0から各ルートのクラスに _$RouteName のmixinを追加する必要があります。これを追加しないとビルド時にエラーとなるため注意が必要です。

アップデート手順

  1. pubspec.yamlのバージョン更新
  2. flutter pub get の実行
    • この時点で dart run build_runner build --delete-conflicting-outputs を実行すると、各ルートのクラスでmixinが不足している旨の警告が表示されます。
      [SEVERE] go_router_builder on lib/router/router.dart:
      
      Missing mixin clause `with _$HogeRoute`
      package:sample/router/router.dart:51:7
         ╷
      51 │ class HogeRoute extends GoRouteData {
         │       ^^^^^^^^^
      
  3. コード修正
    • Before
      class HogeRoute extends GoRouteData {
        const HogeRoute();
        
        Widget build(BuildContext context, GoRouterState state) {
          return const HogePage();
        }
      }
      
    • After
      // _$HogeRouteを追加する
      class HogeRoute extends GoRouteData with _$HogeRoute {
        const HogeRoute();
        
        Widget build(BuildContext context, GoRouterState state) {
          return const HogePage();
        }
      }
      
  4. コード生成の再実行
    • dart run build_runner build --delete-conflicting-outputs

ハマったポイント

全てのルーティングを1つのファイルで行っている場合は、前述のアップデート手順だけ行えば問題ありません 👍️

しかし、可読性の観点からルーティングを複数ファイルに分割している場合は注意が必要です。
アプリにボトムナビゲーションがあるとルーティングファイルが肥大化してしまうので、ファイルを分割して管理している方も多いと思います。

前提

下記のファイル構成でルーティングを管理していたとします。

router/
├── router.dart                # ルート全体のエントリーポイント
└── bottom_nav_router/
    ├── home_router.dart        # ホーム画面用ルート
    └── settings_router.dart    # 設定画面用ルート
// router/router.dart

import 'package:sample/features/tutorial/presentations/tutorial_page.dart';

part 'router.g.dart';

<BottomNavShellRoute>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    homeStatefulShellBranch,
    settingsStatefulShellBranch,
  ],
)

class BottomNavShellRoute extends StatefulShellRouteData {
   // 略
}

<TutorialRoute>(
  path: '/tutorial',
)
class TutorialRoute extends GoRouteData with _$TutorialRoute {
  const TutorialRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const TutorialPage();
}

// 他の画面など
// router/bottom_nav_router/home_router.dart

import 'package:sample/features/home/presentations/home_page.dart';

const homeStatefulShellBranch = TypedStatefulShellBranch<HomeBranch>(
  // ...
)

class HomeBranch extends StatefulShellBranchData {
  // ...
}


class HomeRoute extends GoRouteData with _$HomeRoute {
  const HomeRoute();

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

// 他の画面など

この状態だと何がまずいのか

この状態のままコード生成を行うと、router/ 配下にコードが生成されます。

router/
├── router.dart                # ルート全体のエントリーポイント
├── router.g.dart              # go_router_builderによって生成されたファイル
└── bottom_nav_router/
    ├── home_router.dart        # ホーム画面用ルート
    └── settings_router.dart    # 設定画面用ルート

go_router_builderで生成されるコードは、エントリーポイントの自動生成ファイルであるrouter.g.dartに全て集約されます。
home_router.dartにあるHomeRoute関連の定義もrouter.g.dartに生成されます。
つまり、今回追加したい_$HomeRouterouter.g.dartに生成されます。
また、_$HomeRouteはプライベートなmixinなのでhome_router.dartからは参照できません。

class HomeRoute extends GoRouteData with _$HomeRoute // ← ここが参照できない

・・・。詰んだ😇

でも大丈夫です!解決策を見つけました!

最初に選択しようとした解決策:router.dartに全部まとめる

ファイル分割を諦めて、router.dartに全て集約すれば解決します!
ただ、あまりスマートじゃないよなぁと後ろ髪を引かれていました。
せっかくファイル分割して可読性が良かったのに。。。😵‍💫

最終的に採用した解決策:partpart ofでまとめる

他に方法無いかなぁとrouter.g.dartを眺めていたら、ファイルの先頭に

part of 'router.dart';

と書いてあり、「この書き方ってなんなの?」と思って調べてみたらまさに今回の問題をドンピシャで解決できる書き方でした。

partpart ofとは

Dartのpartpart ofは、1つのライブラリ(ファイル)を複数のファイルに分割して管理するための仕組みです。
主に大きなクラスや複数の関連クラスをファイル分割したいときに使います。

  • partで指定したファイルは、親ファイルの一部として扱われます。
  • part ofで親ファイル名を明示することで、親ファイルのプライベートを参照できます。

修正後の実装

// router/router.dart

import 'package:sample/features/home/presentations/home_page.dart';  // 追加
import 'package:sample/features/tutorial/presentations/tutorial_page.dart';

part './bottom_nav_router/home_router.dart'; // 追加
part './bottom_nav_router/settings_router.dart'; // 追加
part 'router.g.dart';

// ...既存実装...
// router/bottom_nav_router/home_router.dart

// part ofを記載する場合はpart of以外を宣言できません
// importは親ファイルであるrouter.dartに書きます
//import 'package:sample/features/home/presentations/home_page.dart';

part of '../router.dart';

// ...既存実装...

// router.dartの一部なので_$HomeRouteを参照できる
class HomeRoute extends GoRouteData with _$HomeRoute { 
  // ...
}

まとめ・所感

無事アップデートできてよかったです🍻
partpart ofについては、RiverpodやFreezed等のコード生成でも目にしていたのですが、あまり気に留めずに使っていました。
今回の知見を応用して、肥大化してしまった他のファイルについても適切なサイズにリファクタしていきたいと思います🙌

東急URBAN HACKS

Discussion