開発Tips

ログ簿をネイティブAndroidからFlutterへ移行した話

「ログ簿」はもともと Kotlin で書かれたネイティブ Android アプリでした。iOS 版も出したい、けれど一人で二つのネイティブコードベースを保守し続けるのは現実的ではない——そこで Flutter への移行に踏み切りました。本記事では、移行の全体像と設計判断、そして途中で踏んだ落とし穴を残しておきます。

なぜ移行したのか

  • iOS/Android の二重保守を一本化したい。 個人開発で使える時間は限られており、UI とロジックを共通化できる効果は大きい。
  • UI をゼロから作り直す良い機会だった。 ネイティブ側はすでに Jetpack Compose 化を終えていて、宣言的 UI への移行は済んでいた。Flutter の宣言的 UI とも発想が近く、移植のイメージが付きやすかった。

アーキテクチャの方針

移行にあたって、Clean Architecture + Riverpod を採用しました。レイヤーは次のように分離しています。

  • UI 層 … 画面 Widget と Notifier(Riverpod)
  • Domain 層 … ドメインモデルと Repository インターフェース
  • Data 層 … Repository 実装、ローカル DB、API クライアント

依存の向きは常に「UI → Notifier → Repository(インターフェース)← 実装」に固定し、UI がデータの詳細を知らないようにしています。DB には ObjectBox、状態管理は Riverpod、ネットワークは Dio + Retrofit という構成に落ち着きました。

移行で踏んだ落とし穴

移行は「UI を書き直せば終わり」ではなく、プラットフォーム/ライブラリ境界でいくつも足を取られました。代表的なものを挙げます。

1. データ移行(旧Android DB → ObjectBox)

既存ユーザーの記録を失わないため、ネイティブ Android 時代のローカルデータを Flutter 版の ObjectBox へ引き継ぐ必要がありました。ポイントは DB を開く前(openStore() の前)に移行処理を走らせること。順序を誤ると、空のストアが先に確定してしまい移行できません。

2. メジャーバージョン移行の連鎖

移行の途中で Riverpod・ObjectBox・build_runner のメジャーアップデートが重なり、生成コードとランタイムの噛み合わせで詰まりました。コード生成系はバージョンを合わせて一度に上げる方が、部分的に上げるより結果的に安全でした。

3. ネイティブライブラリのロード問題

一部端末で起動時にネイティブライブラリのロードに失敗するクラッシュに遭遇しました。ビルド設定(ネイティブライブラリの同梱・展開方法)に依存する環境固有の問題で、再現条件の切り分けに時間を使いました。この手の問題は「手元で再現しない」ことが多く、ログと端末条件を突き合わせて地道に絞り込むしかありません。

移行してよかったこと

  • iOS/Android のUI・ロジックが一本化され、機能追加のコストが体感で大きく下がった。
  • Clean Architecture で層を切ったことで、テスト(Widget テスト・ユニットテスト)が書きやすくなった。

まとめ

ネイティブ → Flutter の移行は、UI の書き換えよりも データ移行・ライブラリ境界・ビルド設定といった「地味だが事故りやすい部分」にこそ時間がかかります。逆に言えば、そこを設計段階で潰しておけば、移行後の開発体験は大きく改善します。

なお、ローカルDBに ObjectBox を選んだ理由 は別記事にまとめました。個別のハマりどころ(DB 移行・ネイティブライブラリのクラッシュ)も順次掘り下げていきます。