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() の前に、冪等に、一度だけ——この順序さえ最初に固定すれば、変換自体は素直に書けます。逆にここを曖昧にすると、「一部ユーザーだけデータが消える」という、再現しづらく影響の大きい不具合につながります。