「設定の保存?SharedPreferencesで十分でしょ」
──そう言って、何の疑いもなくapply()
やcommit()
を使っていたあの頃の自分に、「ちょっと待て」と声をかけたい。
ある日、あるアプリの利用ユーザー全員から「設定がリセットされた」という問い合わせが殺到。
調査してもログは残らず、変更履歴にもバグは見当たらない。
それでも、確かにSharedPreferencesは何かの拍子に“初期化”されていた。
今回は、SharedPreferencesにまつわる「全初期化事件」と、その背後に潜んでいた落とし穴を語ります。
事件の始まり:ユーザーの声で気づく“異変”
ある朝、カスタマーサポートからSlackに悲鳴が上がりました。
「ユーザー設定が全部リセットされているって苦情が…!」
「昨日のアップデート以降、通知もオフになってるみたい…」
慌ててFirebase CrashlyticsやLogcatをチェックしましたが、エラーは皆無。
アプリは正常に動いており、クラッシュもしていませんでした。
ただ、ユーザーの設定値──とくに通知のON/OFF、テーマ、アカウント連携フラグなどが、すべてデフォルトに戻っていたのです。
原因を追え:ログが残らない「消え方」
まず、SharedPreferencesに保存していた設定項目をすべて洗い出し、getSharedPreferences()
で取得するパスをログで出力。
val prefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
Log.d("PrefsPath", prefs.toString())
結果、パスやファイルは正しく存在していたのに、値がなかった。
prefs.getBoolean("isNotificationEnabled", true)
→ true(デフォルト)prefs.getString("theme", "light")
→ “light”(デフォルト)
これは、SharedPreferencesのファイル自体がリセットされたことを意味していました。
疑わしい犯人その1:clear()の誤用
当初は自分のコードの中にprefs.edit().clear()
が混入していたのではないかと疑いました。
例えば、ログアウト処理などでやりがちなこのコード:
prefs.edit().clear().apply()
一見して「ログイン情報だけ消す」ように見えますが、このコードは「すべてのキーを消す」動作になります。
さらに最悪なのは、複数のSharedPreferences
をまとめて扱っている場合。
ログアウト時に別の設定までclear()
されてしまい、ユーザーの通知設定やUIテーマまでも初期化されていたのです。
疑わしい犯人その2:Context.MODE_PRIVATEの誤解
もう一つの原因は、同じファイル名を異なるスコープ(Context)で取得していたことです。
たとえば、アプリ内のあるActivity
ではこのように取得:
val prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
一方で、カスタムViewやServiceでは、applicationContext
を使って以下のようにアクセス:
val prefs = context.applicationContext.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
このスコープの違いが、端末やAPIレベルによって異なるファイルとして扱われたり、アクセスタイミングで競合が発生することがあるのです。
また、旧バージョンで保存された形式が新バージョンで読み取れない(例:暗号化ライブラリの変更)といったケースも原因になりえます。
本当のトリガーは「端末のストレージ圧迫」
最終的な調査で明らかになった原因は、一部端末でストレージが圧迫された際にSharedPreferencesのファイルがOSによって削除されたというものでした。
特に低スペック・低容量端末では、OSがアプリのキャッシュや一時ファイル、SharedPreferencesを「不要なファイル」とみなして削除することがあるという情報がありました。
/data/data/[package]/shared_prefs/user_prefs.xml
このファイルが、アプリは正常でも、ファイル単位で消されていた。
それが、あの“静かなるリセット”の真相でした。
対策:SharedPreferencesを使うときに心がけるべきこと
今回の事件を経て、私は以下のようにSharedPreferencesの扱いを見直しました。
保存する値を「慎重に」選ぶ
- 認証情報やアプリの根幹に関わる設定は、SharedPreferencesではなくEncryptedSharedPreferencesやRoom/Proto DataStoreに移行。
- SharedPreferencesはあくまで「軽い一時設定」用と割り切る。
clear()の範囲を限定する
- ログアウト時に全設定を消さないよう、キーを指定して削除。
prefs.edit().remove("login_token").apply() // これだけ消す
apply()ではなくcommit()を重要箇所では使う
apply()
は非同期なため、すぐにプロセスが落ちると書き込みが失敗する可能性がある。
- 保存が確実に必要な場面(初回起動のフラグなど)では
commit()
で同期保存。
保存成功後にログを出す
val result = prefs.edit().putBoolean("isLoggedIn", true).commit()
if (!result) {
Log.e("Prefs", "保存失敗!")
}
最後に:SharedPreferencesは万能じゃない
SharedPreferencesは便利です。
でも、OSのバージョン差、ファイル競合、非同期処理、ストレージ状態など、多くの不確定要素に依存しています。
“動いているように見えるけど、実は危うい”
そんな機能ほど、「なぜ動くか」ではなく「なぜ危険か」を理解することが重要だと感じました。
あのときの「一晩で設定がすべて消えた」という体験は、SharedPreferencesに対する信頼を完全に揺るがすには十分すぎるものでした。