Preferences DataStore
Why DataStore?
SharedPreferences has several problems:
- Synchronous API:
commit()blocks the main thread;apply()is unreliable during process death - No type safety: keys are strings, values are untyped
- No error handling: corrupted files crash the app
- No atomic operations: multi-step edits are not safe
DataStore solves all of these. It uses Kotlin coroutines and Flow, is fully async, and supports type-safe keys.
Setting Up Preferences DataStore
val Context.dataStore by preferencesDataStore(name = "settings")
This creates a singleton DataStore tied to the application context. The by delegate ensures only one instance exists.
Defining Keys
object PreferenceKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val USERNAME = stringPreferencesKey("username")
val FONT_SIZE = intPreferencesKey("font_size")
val LANGUAGE = stringPreferencesKey("language")
}
Writing Data
suspend fun Context.setDarkMode(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PreferenceKeys.DARK_MODE] = enabled
}
}
suspend fun Context.setUsername(name: String) {
dataStore.edit { preferences ->
preferences[PreferenceKeys.USERNAME] = name
}
}
edit is a suspend function that provides a MutablePreferences object. All writes are atomic and thread-safe.
Reading Data
val Context.darkModeFlow: Flow<Boolean> = dataStore.data
.map { preferences ->
preferences[PreferenceKeys.DARK_MODE] ?: false
}
val Context.usernameFlow: Flow<String> = dataStore.data
.map { preferences ->
preferences[PreferenceKeys.USERNAME] ?: "Guest"
}
Each Flow emits the current value and re-emits whenever the key changes. Use ?: defaultValue for first-run behavior.
Using in ViewModel
class SettingsViewModel(private val context: Context) : ViewModel() {
val isDarkMode: StateFlow<Boolean> = context.darkModeFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false
)
fun toggleDarkMode() {
viewModelScope.launch {
context.setDarkMode(!isDarkMode.value)
}
}
}
WhileSubscribed(5000) keeps the Flow active for 5 seconds after the last subscriber disconnects, avoiding restarts during configuration changes.
Migration and Type-Safe Proto DataStore
Migrating from SharedPreferences
DataStore provides a built-in migration from SharedPreferences:
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs"))
}
)
This reads keys from the old SharedPreferences file and writes them to DataStore on first access. After migration, the old file is not deleted automatically — you can remove it yourself.
Handling Multiple Preferences Files
If you have multiple SharedPreferences files, create separate DataStore instances:
val Context.settingsStore by preferencesDataStore(name = "settings")
val Context.authStore by preferencesDataStore(name = "auth")
Each name maps to a separate file. Never use the same name for two DataStore instances.
Type-Safe Proto DataStore
Proto DataStore uses Protocol Buffers for strongly typed data. Define your schema in a .proto file:
syntax = "proto3";
option java_package = "com.example.app";
message UserPreferences {
bool dark_mode = 1;
string username = 2;
int32 font_size = 3;
}
Create a serializer:
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return try {
UserPreferences.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto", e)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
Proto DataStore is preferred for complex schemas. Preferences DataStore is simpler for flat key-value storage.
Error Handling
DataStore wraps errors in DataStoreException. Wrap reads in try-catch for corrupted data:
val safeFlow: Flow<Int> = dataStore.data
.catch { exception ->
if (exception is CorruptionException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { it[PreferenceKeys.FONT_SIZE] ?: 14 }
Quiz
1. What are the main problems with SharedPreferences that DataStore solves?
2. What does the preferencesDataStore delegate return?
3. What is the purpose of SharingStarted.WhileSubscribed(5000)?
4. When should you prefer Proto DataStore over Preferences DataStore?
Flashcards
Question
What is DataStore and why does it replace SharedPreferences?
Click to reveal answer
Answer
DataStore is a coroutine-based, type-safe key-value or proto storage solution. It replaces SharedPreferences with async APIs, type safety, error handling, and atomic operations.
Question
What are the two types of DataStore?
Click to reveal answer
Answer
Preferences DataStore for simple key-value storage (similar to SharedPreferences). Proto DataStore for strongly typed structured data using Protocol Buffers.
Question
How do you migrate from SharedPreferences to DataStore?
Click to reveal answer
Answer
Use SharedPreferencesMigration in the preferencesDataStore delegate. It reads keys from the old file and writes them to DataStore on first access.
Question
What does dataStore.edit { } do?
Click to reveal answer
Answer
Provides a suspend function with a MutablePreferences object for atomic writes. All modifications within the block are applied together, preventing partial updates.
Revision Notes
Key Takeaways
- 1. DataStore is coroutine-based, fully async, and replaces SharedPreferences
- 2. Preferences DataStore uses type-safe keys and Flow for reactive reads
- 3. edit{} provides atomic multi-key writes that are thread-safe
- 4. SharedPreferencesMigration imports existing data on first access
- 5. Proto DataStore is preferred for complex, structured data schemas
Interview Tips
- • Explain the problems with SharedPreferences and how DataStore solves them
- • Demonstrate reading and writing Preferences DataStore with Flows
- • Discuss the migration path from SharedPreferences to DataStore
- • Know when to use Preferences DataStore vs Proto DataStore
Cheat Sheet
DataStore Cheat Sheet
Setup:
val Context.dataStore by preferencesDataStore(name = "name")- Singleton per name, tied to application context
Keys:
booleanPreferencesKey("key")stringPreferencesKey("key")intPreferencesKey("key")floatPreferencesKey("key")longPreferencesKey("key")
Reading:
dataStore.data.map { it[key] ?: default }— Flow-based- Collect with
collectAsStateWithLifecycle()in Compose - Use
catchfor error handling
Writing:
dataStore.edit { it[key] = value }— suspend, atomic- Thread-safe and process-safe
Migration:
SharedPreferencesMigration(context, "old_prefs")- Runs on first DataStore access
Proto DataStore:
.protoschema file- Custom
Serializer<T>implementation - Strongly typed, complex data structures
StateFlow Integration:
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), default)