For over a decade, SharedPreferences was the default choice for storing simple key-value data in Android. It was easy to use and got the job done. But as Android development has matured, its flaws have become more apparent and more dangerous.

Google's modern replacement is Jetpack DataStore, a robust, safe, and asynchronous data storage solution. Migrating might seem like a chore, but it's one of the best quality-of-life improvements you can make to your app. This guide will show you how to perform a seamless, one-time migration from SharedPreferences to Preferences DataStore.

Why Bother Migrating? The Problems with SharedPreferences

The biggest problem with SharedPreferences is that its API is synchronous. Calling apply() or commit() can block the UI thread, leading to jank or even ANRs.

Beyond that, it has other significant issues:

  • No Error Handling: If something goes wrong while parsing the XML file on disk, your app will crash. There's no built-in way to handle this.
  • Not Fully Transactional: The API lacks strong guarantees for data consistency.
  • No Type Safety: You're just dealing with strings and primitives, which can lead to runtime errors if keys are misspelled.

Jetpack DataStore solves all of these problems by using Kotlin Coroutines and Flow to provide a fully asynchronous, transactional, and safer API.

The Safe Migration Plan

The DataStore library provides a built-in migration tool that makes this process incredibly safe. It will read your existing SharedPreferences file once, move the data into the new DataStore file, and then you'll exclusively use the DataStore API from that point forward.

Step 1: Add the DataStore Dependency

You only need one dependency for Preferences DataStore.

// In app/build.gradle.kts
implementation("androidx.datastore:datastore-preferences:1.1.1") // Use latest

Step 2: Create the DataStore Instance

Instead of getting an instance like you do with SharedPreferences, you create a top-level property delegate. This is typically done in a file outside of any class, like `AppSettings.kt`.

Creating the DataStore with a Migration
private const val USER_PREFERENCES_NAME = "user_settings"

// This is the name of your old SharedPreferences file
private const val LEGACY_SHARED_PREFS_NAME = "my_app_legacy_prefs"

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    // This is the magic part!
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, LEGACY_SHARED_PREFS_NAME))
    }
)

// Define the keys you will use
object UserPreferencesKeys {
    val USER_THEME = stringPreferencesKey("user_theme")
    val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
}

Step 3: Reading Data from DataStore

Reading from DataStore is reactive. You don't just "get" a value; you observe a Flow of values. This means your UI will automatically update whenever the value on disk changes.

// In a repository or ViewModel
val userThemeFlow: Flow<String> = context.dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException if there was an error reading data
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get the theme, defaulting to "light" if not set
        preferences[UserPreferencesKeys.USER_THEME] ?: "light"
    }

Step 4: Writing Data to DataStore

Writing is a suspend function, so it must be called from a coroutine. The edit block is transactional, ensuring that the update is atomic.

// In a repository or ViewModel
suspend fun setNotificationsEnabled(enabled: Boolean) {
    context.dataStore.edit { preferences ->
        preferences[UserPreferencesKeys.NOTIFICATIONS_ENABLED] = enabled
    }
}

By following these steps, you've successfully migrated. The first time your app accesses the DataStore, the SharedPreferencesMigration will run, safely copy the data, and from then on, your app will be using the modern, safer DataStore API. It's a one-time investment that pays huge dividends in app quality and stability.