Android Academy · Lesson

DataStore Preferences

Replace SharedPreferences with the modern DataStore library. Read data as a Kotlin Flow, write with edit{}, and handle errors safely.

Lesson 5 of 613 steps

DataStore Preferences is a free Android Academy lesson on CoddyKit. This is lesson 5 of 6. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, and your progress syncs across the web and the CoddyKit app. The Android Academy course includes 6 lessons in total.

Why DataStore?

DataStore is the modern replacement for SharedPreferences. It solves real problems:

  • SharedPreferences blocks the UI thread — DataStore is fully asynchronous
  • SharedPreferences can throw uncaught exceptions — DataStore handles errors via Flow
  • DataStore is type-safe (Preferences) or schema-safe (Proto)
  • Works natively with Kotlin Coroutines and Flow

Two Types of DataStore

Choose the right type for your use case:

  • Preferences DataStore — key-value pairs, no schema needed. Simplest to set up. Best for app settings, user preferences.
  • Proto DataStore — typed objects using Protocol Buffers. Requires a .proto schema file. Best when you have structured data.

This lesson covers Preferences DataStore.

Adding the Dependency

Add DataStore to app/build.gradle:

// app/build.gradle
dependencies {
    implementation 'androidx.datastore:datastore-preferences:1.1.1'
    // Coroutines (likely already in your project)
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}

Creating a DataStore Instance

Create the DataStore once using the top-level delegate. Typically done in a standalone file:

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

// Top-level property — creates a single DataStore instance per Context
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")

Defining Keys

Keys are type-safe objects, not plain strings. Create them with the right factory function:

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

object PreferencesKeys {
    val DARK_MODE    = booleanPreferencesKey("dark_mode")
    val USER_NAME    = stringPreferencesKey("user_name")
    val LAUNCH_COUNT = intPreferencesKey("launch_count")
}

Writing Data

Write inside a coroutine using edit { }:

import androidx.datastore.preferences.core.edit

// In a ViewModel or Repository:
suspend fun saveDarkMode(enabled: Boolean) {
    context.dataStore.edit { prefs ->
        prefs[PreferencesKeys.DARK_MODE] = enabled
    }
}

suspend fun incrementLaunchCount() {
    context.dataStore.edit { prefs ->
        val current = prefs[PreferencesKeys.LAUNCH_COUNT] ?: 0
        prefs[PreferencesKeys.LAUNCH_COUNT] = current + 1
    }
}

Reading Data with Flow

DataStore exposes data as a Flow. Use map to extract the value you need:

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

val darkModeFlow: Flow<Boolean> = context.dataStore.data
    .map { prefs ->
        prefs[PreferencesKeys.DARK_MODE] ?: false  // default = false
    }

val userNameFlow: Flow<String> = context.dataStore.data
    .map { prefs ->
        prefs[PreferencesKeys.USER_NAME] ?: "Guest"
    }

Collecting in ViewModel

Expose DataStore Flows from a ViewModel using stateIn for efficient collection:

class SettingsViewModel(private val repo: SettingsRepository) : ViewModel() {

    val isDarkMode: StateFlow<Boolean> = repo.darkModeFlow
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = false
        )

    fun toggleDarkMode(enabled: Boolean) {
        viewModelScope.launch {
            repo.saveDarkMode(enabled)
        }
    }
}

Observing in Activity/Fragment

Collect the StateFlow in the UI using lifecycleScope.launch:

// In Fragment:
lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.isDarkMode.collect { darkMode ->
            binding.switchDarkMode.isChecked = darkMode
            AppCompatDelegate.setDefaultNightMode(
                if (darkMode) AppCompatDelegate.MODE_NIGHT_YES
                else AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
            )
        }
    }
}

Error Handling

DataStore emits IOException if the file is corrupted. Catch it in your Flow pipeline:

import kotlinx.coroutines.flow.catch
import java.io.IOException

val safeFlow: Flow<Boolean> = context.dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())   // return defaults
        } else {
            throw exception
        }
    }
    .map { prefs ->
        prefs[PreferencesKeys.DARK_MODE] ?: false
    }

DataStore vs SharedPreferences

Summary of why DataStore wins:

  • DataStore reads/writes on a background thread automatically — no ANR risk
  • SharedPreferences apply() can silently fail; DataStore propagates errors via Flow
  • DataStore is reactive — UI updates automatically when data changes
  • SharedPreferences has no migration path; DataStore has a built-in SharedPreferencesMigration

Quick Check

What is the main mechanism DataStore uses to expose its data to the rest of the app?

Recap: DataStore Preferences

DataStore is the modern way to persist simple data:

  • Add datastore-preferences dependency
  • Create a single instance with preferencesDataStore delegate
  • Define type-safe keys with stringPreferencesKey, booleanPreferencesKey, etc.
  • Write with dataStore.edit { } inside a coroutine
  • Read as a Flow with dataStore.data.map { }
  • Handle IOException with .catch { emit(emptyPreferences()) }

Next: build efficient list adapters with DiffUtil.

Free to start

Learn Kotlin with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
36
Lessons
152

Frequently Asked Questions

Is the “DataStore Preferences” lesson free?

Yes — any 3 lessons of the Android Academy learning path, including “DataStore Preferences”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Android Academy course includes 6 lessons in total.

What will I learn in “DataStore Preferences”?

Replace SharedPreferences with the modern DataStore library. Read data as a Kotlin Flow, write with edit{}, and handle errors safely. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 5 of 6.

How long does the “DataStore Preferences” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Kotlin Collections
  2. RecyclerView Basics
  3. RecyclerView Click Events
  4. SharedPreferences
  5. DataStore Preferences
  6. Adapters & DiffUtil
← Back to Android Academy