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.

Note: All the Kotlin above is correct, real DataStore code, but this website's own project doesn't run a live Android Studio build, so no value is actually being written or read on this page.
Next up: Simple settings are one thing — TrailLog also needs to store an entire, growing list of trails. Room Database covers structured, queryable local storage.