SharedPreferences (Java)
This lesson starts the Data & Storage section of the track. Every app eventually needs to remember something small between launches — a setting, a flag, a preference — and Android's simplest tool for that is SharedPreferences: a lightweight key-value store, backed by an XML file the system manages for you, meant for small pieces of app configuration rather than large or structured data.
1. Getting an Instance
There are two common ways to obtain a SharedPreferences object:
// Option 1: a named preferences file, scoped to your app
SharedPreferences prefs = getSharedPreferences("recipebox_prefs", MODE_PRIVATE);
// Option 2: the app-wide default preferences file
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- getSharedPreferences("recipebox_prefs", MODE_PRIVATE) gives you a preferences file with that specific name — useful when you want to separate concerns (e.g., one file for display settings, another for sync state).
- MODE_PRIVATE means only your own app can read or write this file — the only mode apps should use today.
- PreferenceManager.getDefaultSharedPreferences() returns one shared, app-wide default file, handy when you don't need separate named groups.
2. Writing a Value
SharedPreferences itself is read-only for retrieving data; writing goes through its Editor object, obtained with edit().
SharedPreferences prefs = getSharedPreferences("recipebox_prefs", MODE_PRIVATE);
prefs.edit()
.putBoolean("dark_mode", true)
.apply();
- apply() writes the change to memory immediately and saves it to disk asynchronously in the background — it returns instantly and never blocks the calling thread.
- commit() writes to disk synchronously and returns a boolean indicating success or failure, but it blocks the thread that calls it until the write completes.
- apply() is preferred for almost every case, because blocking the main (UI) thread — even briefly, for a disk write — risks a janky frame or an ANR (Application Not Responding). Reach for commit() only in the rare case where you genuinely need to know synchronously, right now, whether the write succeeded before proceeding.
3. Reading a Value Back
Every getter on SharedPreferences requires a default value, returned when the key has never been set — for example, on the very first launch, before the user has touched the setting at all.
SharedPreferences prefs = getSharedPreferences("recipebox_prefs", MODE_PRIVATE);
boolean darkModeEnabled = prefs.getBoolean("dark_mode", false);
4. Full Example: Persisting RecipeBox's Dark-Mode Preference
The previous lesson group covered Light/Dark Theme and AppCompatDelegate.setDefaultNightMode() for switching themes at runtime — but without persistence, that choice resets every time the app restarts. SharedPreferences closes that gap.
// SettingsActivity.java
public class SettingsActivity extends AppCompatActivity {
private static final String PREFS_NAME = "recipebox_prefs";
private static final String KEY_DARK_MODE = "dark_mode";
private SharedPreferences prefs;
private SwitchCompat switchDarkMode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
switchDarkMode = findViewById(R.id.switchDarkMode);
// Restore the saved preference when the screen opens
boolean isDark = prefs.getBoolean(KEY_DARK_MODE, false);
switchDarkMode.setChecked(isDark);
switchDarkMode.setOnCheckedChangeListener((buttonView, isChecked) -> {
// Persist the new choice
prefs.edit().putBoolean(KEY_DARK_MODE, isChecked).apply();
// Apply it immediately
AppCompatDelegate.setDefaultNightMode(isChecked
? AppCompatDelegate.MODE_NIGHT_YES
: AppCompatDelegate.MODE_NIGHT_NO);
});
}
}
- On every launch, RecipeBox reads dark_mode from SharedPreferences before the toggle is even shown, so the switch reflects the user's last choice.
- Toggling the switch both persists the new value with apply() and immediately applies the theme change — the two go hand in hand.
- Because apply() is asynchronous, toggling the switch feels instant even though the write to disk happens slightly after the UI already responded.
5. Reacting to Changes: OnSharedPreferenceChangeListener
Sometimes another part of the app needs to know a preference changed, without directly triggering the change itself — for example, a base Activity that should re-theme itself if dark mode gets toggled from a Settings screen elsewhere. OnSharedPreferenceChangeListener provides a callback for exactly that.
private final SharedPreferences.OnSharedPreferenceChangeListener listener =
(sharedPreferences, key) -> {
if (KEY_DARK_MODE.equals(key)) {
// React to the change, e.g. recreate() the Activity
}
};
@Override
protected void onResume() {
super.onResume();
prefs.registerOnSharedPreferenceChangeListener(listener);
}
@Override
protected void onPause() {
super.onPause();
prefs.unregisterOnSharedPreferenceChangeListener(listener);
}
6. Honest Limitations
SharedPreferences is genuinely simple and well-understood, but it's worth knowing where it falls short:
- Despite apply()'s asynchronous write, the underlying API is still fundamentally synchronous and blocking under the hood — the entire preferences file is loaded into memory on first access, which can be a real cost if it grows large or is accessed very early in app startup.
- There's no built-in way to observe preference changes reactively as a stream — OnSharedPreferenceChangeListener above is a manual, listener-based callback, not something that composes cleanly with coroutine- or RxJava-style reactive code.
- This is precisely the gap that Kotlin's Jetpack DataStore was later built to address, offering an asynchronous, reactive replacement — worth knowing if you ever branch into this site's Kotlin track.
Troubleshooting
Value not persisting after app restart? → Confirm you're calling .apply() or .commit() on the Editor — forgetting this is the single most common mistake.
Reading the wrong default on first launch? → Double-check the default value passed to the getter matches the behavior you actually want before the user has ever touched the setting.
Two screens showing different values for the same setting? → Verify both are using the exact same preferences file name passed to getSharedPreferences().