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
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 Migrationprivate 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.