Mastering Jetpack DataStore in Android: The Modern Replacement for SharedPreferences

Table of Contents

If you’re still using SharedPreferences in your Android app, it’s time to move forward. Google introduced Jetpack DataStore as a modern, efficient, and fully asynchronous solution for storing key-value pairs and typed objects. In this blog, we’ll break down what Jetpack DataStore is, why it’s better than SharedPreferences, and how you can use it effectively in your Android projects.

What Is Jetpack DataStore?

Jetpack DataStore is part of Android Jetpack and is designed to store small amounts of data. It comes in two flavors:

  • Preferences DataStore — stores key-value pairs, similar to SharedPreferences.
  • Proto DataStore — stores typed objects using Protocol Buffers.

Unlike SharedPreferences, Jetpack DataStore is built on Kotlin coroutines and Flow, making it asynchronous and safe from potential ANRs (Application Not Responding errors).

Why Replace SharedPreferences?

SharedPreferences has been around for a long time but comes with some baggage:

  • Synchronous API — can block the main thread.
  • Lacks error handling — fails silently.
  • Not type-safe — you can run into ClassCastExceptions easily.

Jetpack DataStore solves all of these with:

  • Coroutine support for non-blocking IO.
  • Strong typing with Proto DataStore.
  • Built-in error handling.
  • Better consistency and reliability.

Setting Up Jetpack DataStore

To start using Jetpack DataStore, first add the required dependencies to your build.gradle:

Kotlin
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.datastore:datastore-core:1.0.0"

For Proto DataStore:

Kotlin
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.14.0"

Also, don’t forget to apply the protobuf plugin if using Proto:

Kotlin
id 'com.google.protobuf' version '0.8.12'

Using Preferences DataStore

Step 1: Create the DataStore instance

Jetpack DataStore is designed to be singleton-scoped. The recommended way is to create it as an extension property on Context:

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

Here, preferencesDataStore creates a singleton DataStore instance. This ensures you have a single DataStore instance per file, avoiding memory leaks and data corruption.

Step 2: Define keys

Kotlin
val USER_NAME = stringPreferencesKey("user_name")
val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")

stringPreferencesKey and booleanPreferencesKey help define the keys.

Step 3: Write data

To write data, use the edit function, which is fully asynchronous and safe to call from any thread:

Kotlin
suspend fun saveUserData(context: Context, name: String, isLoggedIn: Boolean) {
    context.dataStore.edit { preferences ->
        preferences[USER_NAME] = name
        preferences[IS_LOGGED_IN] = isLoggedIn
    }
}

Here, edit suspends while the data is being written, ensuring no UI thread blocking.

Step 4: Read data

To read data, use Kotlin Flows, which emit updates whenever the data changes:

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

Here, data is accessed reactively using Kotlin Flow, returns a Flow<String> that emits the username whenever it changes. You can collect this Flow in a coroutine or observe it in Jetpack Compose.

Real-World Use Case: User Login State

Let’s say you want to keep track of whether a user is logged in. Here’s how you do it:

Save login state:

Kotlin
suspend fun setLoginState(context: Context, isLoggedIn: Boolean) {
    context.dataStore.edit { prefs ->
        prefs[IS_LOGGED_IN] = isLoggedIn
    }
}

Observe login state:

Kotlin
val loginState: Flow<Boolean> = context.dataStore.data
    .map { prefs -> prefs[IS_LOGGED_IN] ?: false }

This setup lets your app reactively respond to changes in the login state, such as redirecting users to the login screen or the home screen.

Migrating from SharedPreferences

Jetpack DataStore makes migration easy with SharedPreferencesMigration:

Kotlin
import androidx.datastore.preferences.SharedPreferencesMigration

val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
)
  • Migration runs automatically before any DataStore access.
  • Once migrated, stop using the old SharedPreferences to avoid data inconsistency.

Using Proto DataStore (Typed Data)

Proto DataStore requires you to define a .proto schema file.

Step 1: Define the Proto schema

user_prefs.proto

Kotlin
syntax = "proto3";

option java_package = "com.softaai.sitless";
option java_multiple_files = true;

message UserPreferences {
  string user_name = 1;
  bool is_logged_in = 2;
}

Step 2: Create the serializer

Kotlin
object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return UserPreferences.parseFrom(input)
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

Step 3: Initialize Proto DataStore

Kotlin
val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

Step 4: Update and read data

Kotlin
suspend fun updateUser(context: Context, name: String, isLoggedIn: Boolean) {
    context.userPreferencesStore.updateData { prefs ->
        prefs.toBuilder()
            .setUserName(name)
            .setIsLoggedIn(isLoggedIn)
            .build()
    }
}

val userNameFlow = context.userPreferencesStore.data
    .map { it.userName }

Best Practices

  • Use Proto DataStore when your data model is complex or needs strong typing.
  • Use Preferences DataStore for simple key-value storage.
  • Always handle exceptions using catch when collecting flows.
  • Avoid main-thread operations; DataStore is built for background execution.

Conclusion

Jetpack DataStore is not just a replacement for SharedPreferences; it’s an upgrade in every sense. With better performance, safety, and modern API design, it’s the future of local data storage in Android.

If you’re building a new Android app or refactoring an old one, now’s the perfect time to switch. By embracing Jetpack DataStore, you’re not only writing cleaner and safer code, but also aligning with best practices endorsed by Google.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!