Skip to content
intermediate Phase 5 · Jetpack Libraries

DataStore

Replace SharedPreferences with DataStore for type-safe, asynchronous key-value or proto storage.

40m
2 problems
Topic Progress 0%

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?

Question 1 options

2. What does the preferencesDataStore delegate return?

Question 2 options

3. What is the purpose of SharingStarted.WhileSubscribed(5000)?

Question 3 options

4. When should you prefer Proto DataStore over Preferences DataStore?

Question 4 options

Flashcards

Question

What is DataStore and why does it replace SharedPreferences?

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?

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?

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?

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 catch for 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:

  • .proto schema file
  • Custom Serializer<T> implementation
  • Strongly typed, complex data structures

StateFlow Integration:

  • .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), default)