DataStore & Preferences
๐พ DataStore & Preferences
๐ Why Not SharedPreferences?
SharedPreferences was Android's original key-value storage API โ still seen in older code, but with real problems DataStore was built specifically to fix:
No Built-In Observability
Reading a value is a synchronous, one-time snapshot โ there's no native way to observe a value changing over time without a separate, manually-registered listener callback.
apply() vs commit() Confusion
commit() writes synchronously (risking a main-thread stall); apply() writes asynchronously but silently swallows any failure โ neither is a genuinely safe default, and it's easy to pick the wrong one without realizing it.
๐ Preferences DataStore โ A Flow-Based Key-Value Store
Context.dataStore by preferencesDataStore(...) reuses Kotlin Intermediate Chapter 4's property delegation again โ this creates a single DataStore instance tied to the app's Context, following the same one-shared-instance idea as Chapter 3's Room database and Chapter 4's Retrofit client. .data is itself a Flow<Preferences> โ every read is inherently observable, with no separate listener API needed, and every write is a genuine suspend function rather than a fire-and-forget apply() call.
Preferences DataStore is a real improvement over SharedPreferences, but it's still fundamentally a loosely-typed key-value bag underneath โ stringPreferencesKey("username") and a typo'd stringPreferencesKey("usrname") elsewhere compile fine and silently create two unrelated keys. This is exactly the problem Proto DataStore, next, solves properly.
๐ Proto DataStore โ A Real Typed Schema
Proto DataStore stores a single, strongly-typed object (defined via a .proto schema file and Protocol Buffers code generation) instead of loose string-keyed values:
A Gradle plugin generates a real Kotlin class (UserPreferences) from this schema at build time โ reading and writing become fully type-checked, with no string-keyed lookups anywhere, and no possibility of a typo'd key silently creating a second, unrelated value.
Preferences DataStore vs Proto DataStore
| Preferences DataStore | Proto DataStore | |
|---|---|---|
| Schema | None โ loose string keys | Defined in a .proto file |
| Type safety | Per-key, easy to typo | Fully typed generated class |
| Setup complexity | Low โ no extra build step | Higher โ needs the Proto Gradle plugin |
| Best for | A handful of simple, independent settings | A larger, structured preferences object |
For most small apps, Preferences DataStore is the pragmatic default โ Proto DataStore's extra setup pays off once there's a genuinely complex settings object worth modeling with a real schema, similar to reaching for Room over a flat file once data gets structured enough to need it.
๐ Wiring DataStore Into the Existing Pattern
This slots into exactly the same repository/ViewModel/Hilt pattern already used for Room and Retrofit:
Every piece here โ @Inject constructor (Chapter 5), a Flow exposed as StateFlow via .stateIn(...) (Chapter 3), viewModelScope.launch (Chapter 6) โ is a direct repeat of the architecture already established for Room. DataStore is simply another data source feeding the exact same pipeline.
DataStore vs Browser localStorage
| Browser localStorage | Android DataStore | |
|---|---|---|
| API shape | localStorage.setItem(key, value) โ synchronous | dataStore.edit { } โ suspend, async by design |
| Reading reactively | No built-in mechanism (storage event is limited) | .data is a Flow โ inherently observable |
| Value types | Strings only, manual JSON.parse/stringify for anything else | Typed keys (Preferences), or a real schema (Proto) |
๐ป Coding Challenges
Challenge 1: A Preferences DataStore Setting
Set up a preferencesDataStore named "app_settings", a booleanPreferencesKey for "notifications_enabled", and functions to read it as a Flow<Boolean> (defaulting to true if unset) and to write a new value via a suspend function.
Goal: Practice the basic Preferences DataStore read/write pattern.
Challenge 2: A Settings Repository with Hilt
Wrap Challenge 1's DataStore logic in a SettingsRepository with an @Inject constructor (taking @ApplicationContext Context), exposing notificationsEnabled as a Flow<Boolean> and a suspend setNotificationsEnabled(enabled: Boolean) function.
Goal: Practice wrapping DataStore access in a properly-injected repository, following Chapter 5's pattern.
Challenge 3: A Settings ViewModel
Write a @HiltViewModel SettingsViewModel exposing Challenge 2's notificationsEnabled as a StateFlow<Boolean> via stateIn, plus a toggleNotifications() function that reads the current value and writes its inverse via the repository.
Goal: Practice the complete DataStore โ Repository โ ViewModel chain, end to end.
Room, Retrofit, and DataStore cover the three data sources a typical Android app actually needs โ structured local data, remote data, and simple settings โ all wired through the identical repository โ Hilt-injected ViewModel โ StateFlow โ Compose pipeline established since Chapter 3. The final chapter pulls all of this together into one cohesive architecture.
๐ฏ What's Next
Next chapter โ the final chapter of Course 2: MVVM Architecture โ the repository pattern, clean architecture basics, and separating concerns across everything built so far.