DataStore & Preferences

Android Development โ€” Architecture & Data
Course 2 ยท Chapter 7 ยท DataStore & Preferences

๐Ÿ’พ DataStore & Preferences

Room (Chapter 3) is for structured, queryable data โ€” a list of tasks, notes, users. For small, simple settings (a theme preference, "has the user seen onboarding," a saved username), Jetpack DataStore is the modern tool โ€” Flow-based, coroutine-native, and the direct replacement for the older SharedPreferences API.

๐Ÿ“› 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

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") object PreferencesKeys { val USERNAME = stringPreferencesKey("username") val DARK_MODE = booleanPreferencesKey("dark_mode") }
// Reading โ€” as a Flow, exactly like a Room query val username: Flow<String> = context.dataStore.data .map { prefs -> prefs[PreferencesKeys.USERNAME] ?: "Guest" } // Writing โ€” a suspend function suspend fun saveUsername(context: Context, name: String) { context.dataStore.edit { prefs -> prefs[PreferencesKeys.USERNAME] = name } }

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.

โš  String Keys Are Still Just Strings

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:

// user_prefs.proto syntax = "proto3"; message UserPreferences { string username = 1; bool dark_mode = 2; }

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 DataStoreProto DataStore
SchemaNone โ€” loose string keysDefined in a .proto file
Type safetyPer-key, easy to typoFully typed generated class
Setup complexityLow โ€” no extra build stepHigher โ€” needs the Proto Gradle plugin
Best forA handful of simple, independent settingsA 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:

class SettingsRepository @Inject constructor( @ApplicationContext private val context: Context ) { val username: Flow<String> = context.dataStore.data .map { prefs -> prefs[PreferencesKeys.USERNAME] ?: "Guest" } suspend fun setUsername(name: String) { context.dataStore.edit { prefs -> prefs[PreferencesKeys.USERNAME] = name } } } @HiltViewModel class SettingsViewModel @Inject constructor( private val repository: SettingsRepository ) : ViewModel() { val username: StateFlow<String> = repository.username .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "Guest") fun updateUsername(name: String) { viewModelScope.launch { repository.setUsername(name) } } }

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 localStorageAndroid DataStore
API shapelocalStorage.setItem(key, value) โ€” synchronousdataStore.edit { } โ€” suspend, async by design
Reading reactivelyNo built-in mechanism (storage event is limited).data is a Flow โ€” inherently observable
Value typesStrings only, manual JSON.parse/stringify for anything elseTyped 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.

โ†’ Solution

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.

โ†’ Solution

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.

โ†’ Solution

๐Ÿ’ก Course 2's Data Layer Is Now Complete

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.