2026年のJetpack DataStore:設定を一つも失わずにSharedPreferencesから移行する
SharedPreferencesからJetpack DataStoreへの移行に関する実践ガイド — 非同期の落とし穴、既存の値を保持する移行パス、そしてそれをどうテストするか。
SharedPreferencesは今でも動く。しかし初回アクセス時に同期的なディスク読み込みを行い、コンパイル時の型安全性もなく、リスナーを手作業で組まない限り値の変化を観測する方法もない。Jetpack DataStoreはこの三つをすべて解決しており、2026年時点では使い捨ての単一フラグ以上のものにはGoogleが推奨するデフォルトになっている。多くの移行作業を止めているのは新しいAPIを学ぶことではなく、次の更新で設定をリセットせずに既存ユーザーを移すことだ。
SharedPreferencesの実際の問題点
getSharedPreferences().getString(...)は同期的で軽量に見えるが、プロセス内での初回アクセス時には呼び出し元のスレッドを実際のファイル読み込みでブロックすることがある——多くの場合、アプリ起動中のメインスレッドだ。変更をストリームとして収集する組み込みの方法はなく、OnSharedPreferenceChangeListenerを登録してそのライフサイクルを自分で管理する必要がある。そしてすべての読み込みは文字列ベースだ。キー名のタイプミスは問題なくコンパイルされ、実行時にデフォルト値を返すことで静かに失敗する。
これらはどれもエキゾチックな端のケースではない。これはSharedPreferencesの通常の振る舞いであり、Preferences DataStoreはメンタルモデルをあまり変えずにこれらを置き換えるために特別に作られた。
Preferences DataStore:同じ形を、安全に
Preferences DataStoreのインスタンスは一度だけ構築され、通常はContextの拡張プロパティとして定義する:
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
読み込みはFlowとして返ってくるので、自分で構築する代わりに変更通知を無料で手に入れられる:
val TIMER_MINUTES = intPreferencesKey("timer_minutes")
val timerMinutes: Flow<Int> = context.settingsDataStore.data
.map { prefs -> prefs[TIMER_MINUTES] ?: 25 }
書き込みはsuspend関数を通るので、誤ってメインスレッドから呼ばれることはない:
suspend fun setTimerMinutes(context: Context, minutes: Int) {
context.settingsDataStore.edit { prefs ->
prefs[TIMER_MINUTES] = minutes
}
}
これはSharedPreferencesに十分近いので、書き換え自体は機械的な作業になる。リスクは、ユーザーの端末にすでに存在するデータに何が起きるかという点にすべてかかっている。
見落とされがちな部分:既存の値を移行する
DataStoreにはまさにこのためのSharedPreferencesMigrationが用意されているが、使うことを強制するものが何もないため見逃しやすい——アプリはそれなしでも問題なくビルドされ動作し、ストレージのバックエンドを切り替える更新のタイミングで、すべての設定を静かにデフォルトへリセットしてしまう。
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "settings_prefs"))
}
)
この移行はこのコードが出荷された後、DataStoreが初めて開かれたときに一度だけ実行される。古いSharedPreferencesファイルを読み込み、各エントリを新しいPreferencesストアにコピーし、古いファイルはそのまま残す(削除はしない——移行がどこでも実行されたと確信できた時点で、それとは別に自分で判断すること)。あるキーの型がきれいに対応しない場合——例えば今はListにしたいSet<String>のような場合——読み込み時に型の不一致で例外を投げさせるのではなく、移行のshouldRunMigrationでフィルタするか明示的に変換する。
古いpreferencesファイルの名前を間違えると——パッケージのデフォルト名ではなくカスタム名で作成されていた場合によくある間違いだ——移行はコピーするものを何も見つけられず、すべてのユーザーが更新時にデフォルトへリセットされてしまう。公開する前に、推測するのではなくコードベースにすでにあるcontext.getSharedPreferences("name", MODE_PRIVATE)呼び出しで正確な名前を確認すること。
Preferences DataStoreでは足りないとき
Preferences DataStoreはフラットで文字列ベースのキー空間を保持する——スレッドと観測可能性の問題は解決したが、型安全性の問題は解決していない。Proto DataStoreはキー・バリューの袋を一度.protoファイルで定義するスキーマに置き換えるので、設定オブジェクトはスキーマに一致するかコンパイルされないかのどちらかになる——実行時にタイプミスされたキーからnullが返ってくることはない。設定画面が数個のフラグを超えて成長したとき、あるいはネストしたオブジェクト(独自のサウンド、バイブレーション、静音時間帯フィールドを持つ通知設定など)が、一つの構造化された値の代わりにキー空間の中で3つや4つの別々の名前空間を持つキーとして現れ始めたときには、その追加のセットアップに見合う価値がある。Mintlyでは、タイマーのサウンド、バイブレーション、自動再起動の設定がまさにこの理由で小さなProto DataStoreのメッセージに移行された——関連する設定を一緒に読み書きする必要が出てきた瞬間、フラットなキー・バリューストアはあなたと戦い始める。
新しいAPIではなく移行そのものをテストする
新しい読み書きコードは十分にシンプルなので、テストを省きたくなる。しかし移行はそうではない——これは実データに対して一度だけ、静かに実行され、間違っていてもやり直すチャンスがないこの変更の唯一の部分だ。最小限のテストは、実際のSharedPreferencesファイルを準備し、移行を接続した状態でDataStoreを開き、値が生き残ったことを検証する:
@Test
fun migration_preservesExistingTimerSetting() = runTest {
val prefs = context.getSharedPreferences("settings_prefs", Context.MODE_PRIVATE)
prefs.edit().putInt("timer_minutes", 45).commit()
val dataStore = PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "settings_prefs")),
produceFile = { File(context.filesDir, "test_settings.preferences_pb") }
)
val minutes = dataStore.data.first()[intPreferencesKey("timer_minutes")]
assertEquals(45, minutes)
}
新しい機能ブランチのキーだけでなく、現在本番環境にあるすべてのキーに対して一度これを実行すること——移行は、この書き換えより何年も前に出荷された機能からの設定も含め、実際の端末が蓄積したものすべてを引き継がなければならない。
チェックリスト
SharedPreferencesからDataStoreへの移行をマージする前に:古いpreferencesファイル名がコードベース内の実際のgetSharedPreferences()呼び出しに照らして確認されている、現在使用中のすべてのキーに対してSharedPreferencesMigrationが接続されている、テストが古い形式を準備して各値が生き残ることを検証している、そしてテレメトリが導入済みベースで移行が実行されたことを確認するまで古いファイルは手つかずのまま残されている。ユーザーがまったく気づくべきではない変更のための、ほんの少しの追加の注意——それこそが、設定の移行にとってまさに目指すべきことだ。
// 関連記事
ジャーナルの他の記事
Androidのベースラインプロファイル: 2026年、コールドスタート時間を実際に左右するもの
Android Baseline Profilesの実践ガイド — Macrobenchmarkでの生成方法、Gradleへの組み込み、実際の効果測定、そしてコールドスタートを左右する他の3つの要素。
Mintly を作る: Android がプロセスを殺そうとする中で集中タイマーを正確に保つ
動作中の Pomodoro タイマーは、一度きりのリマインダーより難しい信頼性の問題を抱えている。フォアグラウンドサービスと壁時計ベースの終了時刻で、Mintly が Doze・プロセス終了・画面オフ中のずれをどう乗り越えたかを解説する。
2026年、AndroidにおけるML Kitバーコードスキャン:Stockyが1秒未満でパントリーに商品を追加する仕組み
ML KitとCameraXによる端末内バーコードスキャンの実践的な2026年版ガイド。フォーマットの絞り込み、オフラインの商品検索、そしてパントリーアプリを支える部分使用の計算まで。