※ChatGPTを使用して記事を作成しています。
「設定保存はSharedPreferencesで十分でしょ」と思っていたあの頃。
UIの保存・復元、フラグ管理、オンボーディングの制御……その手軽さゆえに、プロジェクトのあちこちでapply()
連発。
でもある日、「保存されたはずの値が反映されない」という不可解なバグに悩まされることに──。
今回は、SharedPreferencesの落とし穴と、同期・非同期の罠について、私が実際に踏んだ失敗談をお話しします。
事件の発端:onPauseで保存した値が消える?
とある設定画面の実装中、ユーザーが設定を変更すると即座にSharedPreferencesへ保存し、画面を閉じるようにしていました。
override fun onPause() {
super.onPause()
val prefs = getSharedPreferences("setting", Context.MODE_PRIVATE)
prefs.edit().putBoolean("dark_mode", isDarkModeChecked).apply()
}
このコード、何が悪いと思いますか?
一見何の問題もないように見えます。
でも実はこのapply()
、非同期で保存処理をバックグラウンドで行うんです。
画面が閉じられた直後にプロセスがキルされるようなケース(特に低スペック端末、またはAndroid 6.0以前)では、保存処理が完了する前にアプリが落ちることがありました。
結果、次に開いたときには「設定が保存されていない!」というユーザー報告が相次ぐことに。
apply() vs commit() — ちゃんと理解してる?
メソッド | 保存処理 | 戻り値 | 使いどき |
---|---|---|---|
apply() | 非同期(バックグラウンドで保存) | なし | UIスレッドでも安全に使えるが確実性はやや低い |
commit() | 同期(すぐに保存) | 成否(true/false) | 保存の完了を保証したいときに使う |
当時の私は、apply()の非同期性を「なんとなく」理解していたものの、「commit()を使う必要性」までは真剣に考えていませんでした。
が、実際に「保存されない不具合」が起こったことで、その大切さを痛感しました。
どう改善したか?
onPause
のように、プロセス終了がすぐ起きそうなタイミングでの保存処理には、commit()
を使うようにしました。
override fun onPause() {
super.onPause()
val prefs = getSharedPreferences("setting", Context.MODE_PRIVATE)
prefs.edit().putBoolean("dark_mode", isDarkModeChecked).commit()
}
結果、不具合はピタリと止まりました。
もちろん、UIスレッドで重い保存処理をするのは避けるべきです。
が、簡単なブール値や文字列1つ程度であれば、commit()
の負荷は許容範囲内です。
SharedPreferencesのベストプラクティス(2025年版)
現在の私がプロジェクトで使っているSharedPreferencesに関するルールは以下です。
- apply()は原則使用しない
- 保存が確実に必要な場面が多いため。
- 設定保存はViewModelではなくRepositoryレイヤーで集中管理
- バラバラな保存処理を統一・可視化。
- Jetpack DataStoreを検討する
- type-safeでKotlin向け。コルーチン対応で扱いやすい。
補足:apply()が使えるのはこんな時
例えば、UIの一時状態保存(スクロール位置など)など、「保存できなかったとしても致命的でない場面」ならapply()
でもOK。
ただし、非同期保存ゆえに、直後の読み取りには注意が必要。
今回の教訓
apply()
は保存の「意図」を保証するものではない。
保存した「つもり」ではなく、「確実に保存できたか」を意識することが大切。
保存タイミングが重要。onPause
やonDestroy
は慎重に扱おう。