Code Published on the 28th July 2025

Jetpack DataStore: Another Google Adventure in Android Storage Solutions

For a long time, SharedPreferences felt like the natural choice for handling preferences on Android. But as use cases evolve, so do expectations. Maybe it’s time to rethink our habits with a more modern, streamlined approach… though not without a few trade-offs.

Jetpack DataStore: Another Google Adventure in Android Storage Solutions

For years, we relied on SharedPreferences and EncryptedSharedPreferences to store key-value data on Android. Today, Google is starting to deprecate parts of these APIs and nudging us toward a more “modern” alternative: Jetpack DataStore. Embracing cutting-edge tools always makes sense—as long as they don’t overlook essential features. Let’s take a closer look.

Goodbye SharedPreferences, Hello DataStore

With the deprecation notices piling up (see here and here), we decided to try out DataStore. It’s built on Kotlin Coroutines and Flow, making it more efficient and less prone to "Application Not Responding"s compared to the legacy SharedPreferences. Instead of the old synchronous, blocking approach, DataStore reads and writes asynchronously, which is an improvement in many cases.

SharedPreferences vs DataStore in Code

Here’s a simple comparison of how the two work:

SharedPreferences:

val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("key", "value").apply()
val value = sharedPreferences.getString("key", "default")

DataStore:

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

suspend fun saveValue(value: String) {
    context.dataStore.edit { preferences ->
        preferences[stringPreferencesKey("key")] = value
    }
}

val valueFlow: Flow<String?> = context.dataStore.data
    .map { preferences -> preferences[stringPreferencesKey("key")] }

While DataStore introduces a more structured, reactive approach, SharedPreferences is still simpler for basic key-value storage.

The Two Flavors of DataStore

DataStore comes in two different implementations, each suited for different use cases:

  • Preferences DataStore: This is the most direct replacement for SharedPreferences. It allows storing key-value pairs without a predefined schema. It's lightweight and easy to use for simple settings or flags.
  • Proto DataStore: This version uses Protocol Buffers to define the schema for your data. It enforces type safety and is ideal when you want a more structured and scalable storage solution. It does require you to define .proto files and generate Kotlin classes from them ; see the official Proto DataStore codelab for a hands-on example.

Why DataStore is (Technically) Better

Google claims that DataStore is more modern, safer, and scalable. It offers:

  • Asynchronous reads/writes: Unlike SharedPreferences, which performs synchronous operations, DataStore operations are fully asynchronous thanks to Kotlin Coroutines and Flow.
  • Type safety: Proto DataStore lets you define a schema using Protocol Buffers. This strongly types your data and prevents bugs from incorrect key or value types.
  • Jetpack integration: DataStore is part of the Jetpack ecosystem and plays nicely with other architecture components like ViewModel, LiveData, and Navigation.
  • Easy data migration from SharedPreferences: Google provides built-in support to migrate data from SharedPreferences to DataStore.

The Limits of DataStore

Where’s the Encryption?

One major issue with DataStore is the lack of built-in encryption, which EncryptedSharedPreferences previously provided. Google has been completely silent on this issue despite years of developer concerns (see issue tracker). The fact that encryption hasn’t been addressed after so many requests is frustrating for those handling sensitive data.

Even with new operating system versions, per-item encryption is still necessary for many apps handling sensitive information. Android’s default file-based encryption (FBE), introduced in Android 10, only protects data at rest. Once data is loaded into memory, it’s fully exposed unless encryption is handled at the item level.

In response, developers have created solutions like this open-source project: EncryptedDataStore. It works, but requiring external dependencies for such a fundamental feature feels like an oversight.

Multiplatform, but at what cost?

One of DataStore's key advantages is its multiplatform support, meaning it can also be used in iOS and desktop projects. This makes it appealing for teams aiming for a unified codebase. However, there are important caveats to consider:

  • File-based storage: DataStore persists values to a file using a coroutine-based approach. While this is consistent with how it works on Android, it doesn’t align well with iOS conventions, where UserDefaults and Keychain are the go-to tools for storing preferences and sensitive data. On iOS, file-based storage can be seen as unconventional and may not integrate as smoothly with platform-native behavior, such as backup or security expectations.
  • Security expectations: On iOS, storing sensitive data typically involves leveraging the OS-provided secure storage (Keychain). With DataStore offering no built-in encryption, using it cross-platform could pose security inconsistencies unless you implement manual encryption across platforms.

From our perspective, DataStore’s cross-platform support is promising, but it comes with trade-offs. It's most useful in projects already committed to Kotlin Multiplatform, but for teams prioritizing native platform idioms and simplicity, using native storage mechanisms per platform might still be preferable.

Should You Drop SharedPreferences?

TL;DR: No rush. You might not need to at all. Even though SharedPreferences feels a bit outdated, it’s still a valid option. It’s not deprecated, and it might never be. And we have prepared for you some solutions to work around the deprecations and embrace the modernity.

Secured SharedPreferences

Why the deprecation ?

EncryptedSharedPreferences relies directly on the Android Keystore, which has serious limitations: inconsistent behavior across devices, fragile support for encryption modes like AES-GCM, and no clean way to upgrade encryption schemes once deployed. These weaknesses create risks that are hard to fix without breaking compatibility. So Google deprecated Jetpack Security's cryptography APIs and now recommends moving to Tink. Tink offers a consistent, secure, and upgradeable cryptography layer that is independent from device-specific Android behavior. It allows developers to use modern encryption standards without worrying about low-level implementation details or platform fragmentation, and is widely used in Google products.

A Modern Alternative with Tink

Knowing that, we have prepared a lightweight replacement: SecureStorage. Check out the code, it simply:

  • Encrypts both keys and values using Tink’s AEAD encryption.
  • Saves data in standard SharedPreferences files.
  • Has no dependency on deprecated libraries.
  • Has easy integration with existing code.

Example usage:

val securePrefs = SecureSharedPreferences(context, "secure_prefs")

securePrefs.putString("user_token", "abc123")
val token = securePrefs.getString("user_token")

Asynchronous SharedPreferences

One key detail: SharedPreferences operates in-memory, and using .apply() commits data to disk asynchronously, in the background. For many apps with basic needs, SharedPreferences continues to be a simple and effective choice.

For those needing more modern APIs, like reactive state observation or typesafe access, you can actually wrap SharedPreferences in a more structured layer. Using KSP, you can generate an observable, asynchronous wrapper. The code written here is not complete, it just illustrates what is for us a good workaround.

internal data object PreferencesKeys {
    @Preference(type = PreferencesType.String, secure = true, observable = false)
    const val SESSION_TOKEN: String = "session_token"

    @Preference(type = PreferencesType.Boolean, secure = false, observable = true)
    const val DARK_MODE: String = "dark_mode"
}

We defined three parameters in the annotation to guide the processor:

  • type: Specifies the data type. Supported types include String, Boolean, Int, Long, and Float, mirroring SharedPreferences capabilities.
  • secure: Indicates whether the data should be stored in a standard SharedPreferences or an EncryptedSharedPreferences.
  • observable: Enables generation of a Flow for observing changes to the preference—only when needed.

The generated code would give you both regular synchronous accessors and observable state flows:

// sessionToken

internal var Preferences.sessionToken: String?
    get() = getString(key = PreferencesKeys. SESSION_TOKEN, secure = true)
    set(value) {
        setString(key = PreferencesKeys. SESSION_TOKEN, secure = true, value = value)
    }

// darkMode

private val Preferences.darkModeStateFlow: MutableStateFlow<Boolean?>
    get() {
        if (darkModeField == null) {
            darkModeField = MutableStateFlow(
                getBoolean(key = PreferencesKeys.DARK_MODE, secure = false)
            )
        }
        return darkModeField!!
    }

private var darkModeField: MutableStateFlow<Boolean?>? = null

internal val Preferences.darkModeFlow: Flow<Boolean?>
    get() = darkModeStateFlow

internal var Preferences.darkMode: Boolean?
    get() = darkModeStateFlow.value
    set(value) {
        setBoolean(key = PreferencesKeys.DARK_MODE, secure = false, value = value)
        darkModeStateFlow.value = value
    }

This bridges the old with the new—keeping compatibility while embracing reactive principles. Here’s the minimal Preferences interface you'd implement under the hood:

internal interface Preferences {
    fun getString(key: String, secure: Boolean): String?
    fun setString(key: String, secure: Boolean, value: String?)
    // Same for Boolean, Int, Long, Float
}

This was design to be used in a multiplatform app, where the iOS/macOS implementation of this interface could use UserDefaults and Keychain.

Typesafe SharedPreferences

With the same generated code strategy, you can serialize entire data classes to store them as strings. For example:

data class UserProfile(val id: String, val email: String)

val Preferences.userProfile: UserProfile?
    get() = getString("user_profile", secure = true)?.let { json.decodeFromString(it) }
    set(value) {
        setString("user_profile", secure = true, value = value?.let { json.encodeToString(it) })
    }

Conclusion: DataStore Advances, But Gaps Remain

DataStore is a major improvement over SharedPreferences in terms of architecture, scalability, and modern development practices. For new applications that don't require encrypted storage, it's a solid and future-proof choice. However, for teams like ours, the lack of built-in encryption remains a significant limitation.

We faced this during a pentest, where encrypted local storage for sensitive tokens was a hard requirement. Solutions like manually encrypting values or using libraries like encrypted-datastore work well. But in our view, encryption should not be an afterthought or a manual task left to developers. It should be part of the storage system itself—just like it was with EncryptedSharedPreferences.

Today, SharedPreferences is still a valid choice, especially when properly secured with a solution like our SecureStorage, based on Tink. We also built tooling around SharedPreferences, using code generation to replicate much of what makes DataStore appealing, such as asynchronous access patterns and strong type safety. This lets us offer a modern development experience without sacrificing encryption guarantees.

However, if Google wants developers to fully adopt DataStore—and improve app security in the process—they need to provide an official, easy-to-use encryption solution. Otherwise, many teams will continue relying on EncryptedSharedPreferences out of necessity and convenience, even as the underlying crypto APIs grow increasingly outdated.

Rémi

Software Engineer · Mobile

Read his/her presentation

Do not miss any of our news

Every month, receive your share of information about us, such as the release of new articles or products.