Setapp provides 240+ carefully curated apps in one subscription! Get started with a free trial >

For years, Android developers have relied on SharedPreferences for persisting key-value pairs, but it comes with several limitations—synchronous execution, lack of proper error handling, and no clear mechanism for handling large or complex data. With Android Jetpack, Google introduced DataStore, a modern and robust solution designed to replace SharedPreferences while offering better efficiency and scalability.

In this article, we will explore how to migrate from SharedPreferences to Jetpack DataStore using a clean and type-safe Kotlin generics based approach.

If you prefer a video tutorial instead, do check out this YouTube video I made recently:

Why Jetpack DataStore?

Jetpack DataStore provides two solutions for key-value data persistence:

  • Preferences DataStore (similar to SharedPreferences but with better async support)
  • Proto DataStore (uses protocol buffers for type safety)

In this guide, we’ll focus on Preferences DataStore, which allows us to work with key-value pairs just like SharedPreferences but in a more modern, scalable way.

Key Benefits of DataStore:

  • Asynchronous & Coroutine-friendly: Unlike SharedPreferences, DataStore is designed for Kotlin coroutines, preventing UI thread blocking.
  • Type Safety: Proto DataStore ensures strong type safety with protocol buffers.
  • No UI Freezes: Since DataStore runs asynchronously, there are no UI freezes, even when working with large datasets.
  • Single Source of Truth: Designed for a unidirectional data flow pattern, making state management easier.

Migrating from SharedPreferences to DataStore

Let’s assume we previously used SharedPreferences for managing settings in our Android app. Below is a typical Settings class using SharedPreferences in a generic way:

class Settings(context: Context) {
    private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)

    fun <T> get(setting: Setting): T {
        return when (val type = setting.defaultValue) {
            is SettingType.IntValue -> sharedPreferences.getInt(setting.key, type.value) as T
            is SettingType.FloatValue -> sharedPreferences.getFloat(setting.key, type.value) as T
            is SettingType.StringValue -> sharedPreferences.getString(setting.key, type.value) as T
            is SettingType.BooleanValue -> sharedPreferences.getBoolean(setting.key, type.value) as T
        }
    }

    fun <T> set(setting: Setting, value: T) {
        with(sharedPreferences.edit()) {
            when (value) {
                is Float -> putFloat(setting.key, value)
                is String -> putString(setting.key, value)
                is Boolean -> putBoolean(setting.key, value)
            }
            apply()
        }
    }
}

where SettingType is a sealed class that holds the underlying data classes:

sealed class SettingType {
    inline fun <reified T> get(): T {
        return when (this) {
            is IntValue -> value as T
            is FloatValue -> value as T
            is StringValue -> value as T
            is BooleanValue -> value as T
        }
    }

    data class IntValue(val value: Int) : SettingType()
    data class FloatValue(val value: Float) : SettingType()
    data class StringValue(val value: String) : SettingType()
    data class BooleanValue(val value: Boolean) : SettingType()
}

We can define an enum class that uses this sealed class SettingType to define preferences that can be used throughout the app:

enum class Setting(val key: String, val defaultValue: SettingType) {
    USER_NAME("user_name", SettingType.StringValue("John Doe")),
}

This approach works but it is synchronous and lacks structured error handling. Now, let’s migrate this to Jetpack DataStore. And specifically, Preference DataStore.

Step 1: Add Dependencies

Open your build.gradle.kts file and add the DataStore dependency:

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.2")
}

Then, sync your project after adding the dependencies.

Step 2: Setting Up DataStore

Unlike SharedPreferences, DataStore requires defining an extension property in the Context class. This helps in initializing it properly.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Step 3: Create a Generic Preferences Manager

We’ll now create a type-safe generic manager for handling different data types using Kotlin generics.

Define a Generic Preference Class

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

// ...

sealed class Preference<T>(val key: Preferences.Key<T>, val defaultValue: T) {
  companion object {
    val USER_NAME = Preference(stringPreferencesKey("user_name"), "John Doe")
  }
}

Here we map the same Settings enum from the previous SharedPreferences example to something better. Notice how our code is already shorter and we don’t need a SettingType sealed class anymore.

Let’s create our generic class to get and set the Settings through DataStore.

Create a Generic Settings class

import android.content.Context
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class Settings(private val context: Context) {
    fun <T> get(preference: Preference<T>): Flow<T> {
        return context.dataStore.data.map { preferences ->
            preferences[preference.key] ?: preference.defaultValue
        }
    }
    
    suspend fun <T> set(preference: Preference<T>, value: T) {
        context.dataStore.edit { settings ->
            settings[preference.key] = value
        }
    }
}

Step 4: Using DataStore in an Activity or ViewModel

Now, let’s integrate DataStore into our UI. Here’s an example on how to update a value in the DataStore from a ViewModel:

class MainViewModel(private val settings: Settings) : ViewModel() {
    val trimLogs: Flow<Boolean> = settings.getPreference(Preference.TrimLogs)

    fun updateTrimLogs(enabled: Boolean) {
        viewModelScope.launch {
            settings.setPreference(Preference.TrimLogs, enabled)
        }
    }
}

And in your activity:

class MainActivity : AppCompatActivity() {
    private val settings by lazy { Settings(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            settings.getPreference(Preference.TrimLogs).collect { trimLogs ->
                Log.d("DataStore", "Trim Logs: $trimLogs")
            }
        }
    }
}

Using in a Composable

Using the Settings class in a composable is easy too:

@Composable
fun MyComposable(settings: Settings) {
    val trimLogs by settings.getPreference(Preference.TrimLogs).collectAsState(initial = false)

    // Use trimLogs in your UI
}

Wrapping Up

In this guide, we successfully migrated from SharedPreferences to Jetpack DataStore using a clean and generic approach. The new implementation is fully asynchronous, type-safe, and avoids UI-blocking operations.

Key Takeaways

  • Jetpack DataStore is the modern replacement for SharedPreferences.
  • Using Kotlin generics, we can create a flexible and reusable preference manager.
  • DataStore works seamlessly with coroutines and Flow for reactive programming.

If you’re building an Android app in 2025, DataStore is the recommended solution for managing persistent key-value data. 🚀

Have thoughts or questions? Drop a comment below!

Happy coding! 🎉


Join discussions at our discord server >
Did you learn something new? Add a comment below and spread the word!



Contribute to the Genics Blog!

Genics Blog is a purely open source publication. Authors at Genics post highly resourceful content on varied topics relevant to the developer community. Join us and start publishing today!

Contribute

Comments

Related content