DataStore Preferences
This lesson opens the Data & Storage section of the track — everything from here on is about where TrailLog actually keeps its information between launches. We'll start with the smallest kind of persisted data: simple app settings, like the Light/Dark/System theme choice from the previous lesson.
SharedPreferences: The Old Way, and Its Real Problems
For years, SharedPreferences was the standard way to store small key-value settings
— a boolean flag, a saved username, a theme choice. It still works, but it has two real problems
that modern Android APIs were specifically built to fix. First, its commonly-used
commit() call is synchronous and blocking — it can stall the main
thread and cause visible jank if called at the wrong moment. Second, observing changes reactively
means manually registering an OnSharedPreferenceChangeListener and — just as
importantly — remembering to unregister it, or you leak the listener along with whatever it's
attached to.
Jetpack DataStore: Built on Coroutines and Flow
Jetpack DataStore is Google's modern replacement, designed from the ground up
around Kotlin coroutines and Flow rather than bolted onto an older API. Reads and
writes are asynchronous by default, and observing a value for changes is just collecting a
Flow — no listener registration, no manual cleanup.
DataStore comes in two flavors. Preferences DataStore is the simpler one, storing
untyped key-value pairs much like SharedPreferences did, just with a modern API on
top. Proto DataStore stores fully typed, structured objects defined with protocol
buffers — more setup, but stronger guarantees for complex data. This lesson covers Preferences
DataStore, which is the right fit for a handful of simple settings like TrailLog's theme choice.
Setting Up TrailLog's Theme Preference with DataStore
First, a single DataStore<Preferences> instance is created as an extension
property on Context, and a typed key is declared for the value being stored:
Creating the DataStore and declaring a key
val Context.themeDataStore: DataStore<Preferences> by preferencesDataStore(name = "trail_settings")
object ThemeKeys {
val THEME_CHOICE = stringPreferencesKey("theme_choice")
}
stringPreferencesKey() declares a key that stores a String value — here
used to hold "LIGHT", "DARK", or "SYSTEM_DEFAULT". For a
plain on/off flag you'd reach for booleanPreferencesKey() instead; DataStore provides
a matching typed key function for each primitive type it supports.
Writing a Value
Writing is a suspend call — dataStore.edit { } hands you a mutable snapshot of the
preferences to update, and commits it atomically when the block finishes:
suspend fun saveThemeChoice(context: Context, choice: String) {
context.themeDataStore.edit { settings ->
settings[ThemeKeys.THEME_CHOICE] = choice
}
}
Reading It Back as a Reactive Flow
Reading is where DataStore's design really pays off — data exposes the entire
preferences set as a Flow, so mapping out the one key you care about gives you a
Flow<String> that automatically emits a new value the instant it changes,
anywhere in the app:
val themeChoiceFlow: Flow<String> = context.themeDataStore.data
.map { settings -> settings[ThemeKeys.THEME_CHOICE] ?: "SYSTEM_DEFAULT" }
A ViewModel can collect themeChoiceFlow directly as a StateFlow and
apply AppCompatDelegate.setDefaultNightMode() whenever a new value arrives — see
Flow vs LiveData for how collecting a Flow reactively like
this fits into the rest of TrailLog's architecture.
Why This Is Genuinely Nicer Than OnSharedPreferenceChangeListener
With SharedPreferences, reacting to a changed setting means registering a listener
in onStart() (or similar), writing a callback that filters for the specific key you
care about, and remembering to unregister it in onStop() to avoid leaking it. With
DataStore, the exact same reactivity is just a Flow you collect using ordinary
coroutine tools — it automatically stops delivering updates when the collecting scope is
cancelled, so there's no separate cleanup step to forget.
Migrating from an Existing SharedPreferences-Based App
If TrailLog already had settings stored the old way, Jetpack provides a
SharedPreferencesMigration helper that copies existing keys into DataStore
automatically the first time it's read, so existing users don't lose their saved settings during
the switch. This is conceptual awareness for now — worth knowing it exists, not something to build
out in this lesson.