SharedPreferencesでハマった話 ~apply()は非同期って知ってた?~

※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()保存の「意図」を保証するものではない

保存した「つもり」ではなく、「確実に保存できたか」を意識することが大切

保存タイミングが重要。onPauseonDestroyは慎重に扱おう。

タイトルとURLをコピーしました