開発Tips

Flutter化でハマった不具合と解消②: ネイティブAndroidのDBをObjectBoxへ移行する

Flutter 移行で最も神経を使ったのは、UI の作り直しではなく 既存ユーザーのローカルデータを1件も失わないことでした。ログ簿はゲームのデイリー記録を端末内に持つため、ネイティブ Android 時代に蓄積されたデータを、Flutter 版の ObjectBox へそのまま引き継ぐ必要がありました。

前提: 移行は「開く前・一度だけ」

ObjectBox は openStore() でデータベースを開きます。データ移行を実装するうえでの核心は、たった一つでした。

移行処理は openStore() を呼ぶ「前」に、初回だけ実行する。

順序を逆にして先にストアを開いてしまうと、空の ObjectBox ストアが「正しい現在の状態」として確定します。その後で旧データを流し込もうとしても、アプリはすでに空ストア前提で動き始めており、取り込みの隙がありません。データ移行はアプリ起動シーケンスの、ほぼ最初に置く必要があります。

起動シーケンスの設計

main() の中で、Riverpod の ProviderScope を立てたり openStore() する に、移行を試みます。

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. 旧Androidデータが残っていれば ObjectBox に取り込む(初回のみ)
  await _tryMigrate();

  // 2. 移行が済んでから通常どおりストアを開く
  final store = await openStore();

  runApp(
    ProviderScope(
      overrides: [objectboxStoreProvider.overrideWithValue(store)],
      child: const App(),
    ),
  );
}

_tryMigrate() の中身は概ね次の流れです。

Future<void> _tryMigrate() async {
  // すでに移行済みなら何もしない(フラグで冪等性を担保)
  if (await _migrationDone()) return;

  // 旧Androidのローカルデータを読めるか確認
  final legacy = await _readLegacyAndroidData();
  if (legacy == null) {
    await _markMigrationDone(); // 旧データが無い新規ユーザーもフラグを立てる
    return;
  }

  // ObjectBox 用のエンティティに詰め替えて書き込む
  await _importIntoObjectBox(legacy);
  await _markMigrationDone();
}

ポイントは3つです。

  • 冪等にする: 移行完了フラグ(SharedPreferences 等)で、二度目以降は走らせない。
  • 新規ユーザーも通す: 旧データが無いユーザーも「移行済み」にしておくと、毎回判定して無駄に I/O しない。
  • openStore() より前で完結: ストアを開く前に、取り込むか・スキップするかを決め切る。

つまずきやすい点

  • 移行を Provider の初期化後に書いてしまう。 Riverpod のプロバイダ経由でストアを取得する設計だと、つい移行も Provider 化したくなりますが、そうすると「開いた後」になりがちです。移行だけは main() の素朴な手続きとして、開く前に置くのが安全でした。
  • 失敗時の扱い。 取り込み中に失敗したら、完了フラグを立てないこと。次回起動で再試行できるようにしておく。

まとめ

データ移行は「どう変換するか」より 「いつ・何回・どの順序で走らせるか」 の設計が本体でした。openStore() の前に、冪等に、一度だけ——この順序さえ最初に固定すれば、変換自体は素直に書けます。逆にここを曖昧にすると、「一部ユーザーだけデータが消える」という、再現しづらく影響の大きい不具合につながります。