SharedPreferences vs DataStore: Should you migrate?

Migrating away from SharedPreferences isn't about chasing a newer API, it's about eliminating main-thread blocking and silent data loss that SharedPreferences was never built to prevent. For legacy Android apps, the real question isn't whether DataStore is 'better,' but whether the migration risk is worth it given your codebase's size and testing maturity.

In our migrations, the answer depended less on app size and more on how tangled the existing preference keys were. This guide breaks down the actual differences, the decision criteria, and the step-by-step migration path. If you're also weighing decisions about your broader mobile technology stack, our comparison of native, React Native, and Flutter approaches can help inform the bigger picture.

SharedPreferences vs DataStore: The short answer

Migrate from SharedPreferences to Jetpack DataStore once main-thread blocking, corrupted writes, or the need for typed data force the issue. Keep SharedPreferences only for a handful of rarely-read keys in apps you are not actively maintaining.

Jetpack DataStore reached its stable 1.0 release in August 2021, according to Google's AndroidX release notes, and it handles reads through Kotlin Flow instead of the synchronous getters SharedPreferences relies on. In our work migrating production apps, main-thread blocking on cold start was the failure mode SharedPreferences never fixed, since the first read still loads the entire file into memory.

Preferences DataStore covers typed key-value storage; reach for a Room database instead when the data needs relational queries. SharedPreferences read latency runs ~1-2ms lower; write latency is ~4× faster for strings versus Preferences DataStore, according to Pro Android Dev.

What is Jetpack DataStore?

Jetpack DataStore is Google's coroutine-based data storage system, built on Kotlin Flow, that replaces SharedPreferences without the synchronous disk reads that used to freeze the main thread. It ships in two flavors, and the choice between them shapes how you migrate.

Preferences DataStore is the closer analog to SharedPreferences: key-value pairs, but every read returns a Flow<Preferences> and every write runs through a suspend edit{} transform block instead of a fire-and-forget apply. No schema, no type safety beyond what you enforce with preferencesKey<T> calls yourself.

Proto DataStore stores typed objects defined by a protocol buffer schema, read and written through a custom Serializer<T>. You get compile-time type safety and built-in versioning, at the cost of writing a.proto file and a serializer for every stored object.

Both flavors write transactionally, so a process death mid-write leaves the previous value intact rather than a corrupted file, which is one of the recurring failure modes we chased down in legacy SharedPreferences code. According to Android Developers documentation, DataStore backs each Preferences or Proto instance with a single file per process, which is what closes the multi-process race window SharedPreferences never handled cleanly.

That single difference, one asynchronous API instead of two storage models bolted together, is why most migration guides treat DataStore as a rewrite of your data layer, not a drop-in replacement.

SharedPreferences vs DataStore: Key differences

SharedPreferences vs DataStore comes down to one mechanical difference: SharedPreferences reads and writes on the calling thread, so a getString call can cause main-thread blocking the moment the underlying XML file gets large. Jetpack DataStore moves every read and write onto coroutines, backed by Kotlin Flow, so the caller never touches the disk directly.

AndroidX DataStore reached its 1.0.0 stable release in August 2021, which means any app still running SharedPreferences in 2026 has skipped five years of coroutine-native storage tooling that Google now recommends by default.

The table below is the version we hand to teams deciding whether to migrate.

Aspect SharedPreferences Jetpack DataStore
Threading Synchronous; commit blocks the calling thread, apply still risks main-thread stalls on first read Asynchronous by design; every operation runs on Dispatchers.IO and surfaces through Flow<Preferences>
Type safety String keys, loosely typed values, prone to key namespace collisions across app modules Preferences DataStore keeps typed keys; Proto DataStore enforces a schema via protocol buffer definitions
Error handling Corrupted XML fails silently, no exception surfaced to the caller Serializer<T> throws CorruptionException, catchable inside the Flow collector
Testing Needs Context mocking, flaky on CI due to real file I/O Fake DataStore instances inject cleanly via Hilt, no disk access required

In our migrations, the pattern that costs the most engineering time is not the API swap itself, it is the SharedPreferencesMigration API's handling of concurrent writes during rollout, when the old and new store both hold a key mid-transition.

Type safety is what actually changes reviewer behavior: a mistyped string key in SharedPreferences compiles and fails at runtime, while a typed DataStore key fails the build.

For read/write latency at scale, we tell teams to benchmark on their own hardware and key count rather than trust a number we cannot reproduce there.

Preferences DataStore vs proto DataStore: Which to pick

Preferences DataStore stores untyped key-value pairs, similar to SharedPreferences; Proto DataStore stores typed objects defined by Protocol Buffers schemas. Pick Preferences DataStore for simple flags and settings you're migrating from SharedPreferences directly. Pick Proto DataStore when the data has structure worth enforcing at compile time.

The practical difference shows up the moment your app grows past a handful of keys. Preferences DataStore still lets you write prefs[stringPreferencesKey("user_id")] = null by accident, no compiler will stop you. Proto DataStore forces a schema, so a missing field fails the build, not production.

Schema generation is where the tradeoff gets real. Google's official codegen path uses protoc with the standard.proto toolchain, documented on developer.android.com. Some teams swap in Square's Wire code generation instead, mainly to get Kotlin-native generated classes and avoid the Java interop layer protoc produces by default.

We've used Wire on projects already standardized on it for gRPC, and it removes a build step; teams without existing Wire tooling are usually better off with stock protoc, since the official docs and Stack Overflow answers assume it.

Preferences DataStore Proto DataStore
Data model Untyped key-value Typed, schema-defined
Migration source Direct SharedPreferences path Requires a Serializer<T>
Compile-time safety None Full
Best fit Settings, flags Structured app state

Our rule: if you're touching more than 15-20 preference keys during migration, the schema discipline of Proto DataStore pays for itself within two sprints.

Is DataStore actually faster than SharedPreferences?

Speed isn't the real reason to migrate. DataStore's advantage is that it removes main-thread blocking entirely, not that it wins on raw benchmarks for a handful of keys.

SharedPreferences.commit blocks the calling thread until the write hits disk. apply defers the write but still risks an ANR if the OS forces a sync during a lifecycle transition, a known failure sign on low-memory devices.

DataStore has no synchronous API at all. Every read and update goes through Kotlin Flow on Dispatchers.IO, backed by coroutines, so there's no blocking path to accidentally call from the main thread. This design allows every consumer to react to updates without polling.

Most datastore SharedPreferences benchmarks converge on the same conclusion: raw speed is close, main-thread safety isn't. Published Android performance comparisons show SharedPreferences reads running roughly 1-2 ms faster, while write operations on strings and booleans are about 4x faster than Preferences DataStore for single-key updates (Pro Android Dev).

The gap widens under concurrent writes. SharedPreferences' in-memory cache can serialize access and stall callers, while DataStore's transactional edit{} block queues updates without holding a thread.

On one migration moving roughly 40 preference keys off SharedPreferences, the biggest win wasn't in microbenchmarks. It was in removed StrictMode violations for disk I/O on the main thread, violations that had been silently causing jank on lower-end devices for years.

These numbers apply to Preferences DataStore, the key-value variant. ProtoDataStore, which serializes typed objects instead of primitives, trades a little write overhead for compile-time safety, an advantage worth weighing if you're already storing data with well-defined schemas.

If your data needs relational queries, joins, or complex filtering, neither key-value store is the right tool. That's a Room database decision, not a DataStore one.

When should you migrate to DataStore?

The decision comes down to three variables: app size, key count, and how much engineering bandwidth you can spend on the migration itself.

SharedPreferences remains fine for a handful of primitive flags read once at startup. Jetpack DataStore earns its complexity budget once you're storing more than roughly 20 keys, need type safety, or read preferences on a background thread inside a coroutine.

Use this table as a starting filter:

Signal Stick with SharedPreferences Migrate to DataStore
Key count Under ~15, mostly booleans/ints 20+, or growing per release
Data shape Flat primitives Structured objects → Proto DataStore
Concurrency Single writer, rare reads Concurrent reads/writes across modules
Team bandwidth None to spare this quarter Half a sprint for a mid-size app

Structured data changes the answer. If you're persisting anything beyond flat key-value pairs, don't reach for Preferences DataStore at all, go straight to Proto DataStore with a defined schema and a Serializer<T>, since it gives you compile-time type checks that Room database migrations already assume for your relational data.

AndroidX DataStore stable release 1.2.1 (Jetpack - Android Developers).

In our migrations, the SharedPreferencesMigration API handled the mechanical key transfer cleanly, but we still hit two edge cases worth flagging before you commit a team to the work: concurrent writes during staged rollout, and namespace collisions when two feature modules used the same preference key independently.

How to migrate from SharedPreferences to DataStore, step by step

Migrating from SharedPreferences to Jetpack DataStore is a five-step process built around Google's SharedPreferencesMigration API, which handles the one-time transfer transactionally so you never end up with duplicate or half-written keys. In our migrations, this API did the heavy lifting for 90% of the work; the remaining 10% was chasing key namespace collisions across app modules that shared a preferences file name.

If this is part of a larger platform overhaul, it's worth situating this work within the broader application migration process to keep timelines and rollback plans aligned.

  1. Add the dependency. Pull the current stable androidx.datastore:datastore-preferences version from Google Maven, not whatever a two-year-old blog post recommends.
  2. Declare the delegate. Create a preferencesDataStore delegate at file scope, one per logical preferences group, not one giant global store.
  3. Wire the migration. Pass a SharedPreferencesMigration instance into the DataStore builder, pointing at the legacy SharedPreferences file name.
  4. Replace reads. Swap getString/getBoolean calls for Flow<Preferences> collection inside a coroutine on Dispatchers.IO, moving preference reads off the main thread entirely.
  5. Replace writes. Move every write into an edit {} transform block, which is transactional by design and eliminates the torn-write bugs SharedPreferences never guarded against.

The edge cases show up during rollout, not during the code review. Concurrent writes from a background sync job while the UI thread reads the same key can produce stale reads if you don't gate the migration behind a completion flag.

On read/write latency, DataStore's Flow-based API removes the main-thread blocking that SharedPreferences' synchronous apply and commit calls introduce, though we haven't run a public benchmark repo for this migration specifically. SharedPreferences read latency 1-2 ms lower than Preferences DataStore; write latency ~4× faster for strings (Pro Android Dev). Treat any precise millisecond figure you find as directional, not a guarantee for your key count.

Room vs DataStore: Which One to use

Room database and Jetpack DataStore solve different storage problems, not the same one. Room handles structured, relational data, multi-table schemas, foreign keys, joins, exposed through Flow or suspend functions and backed by SQLite. DataStore handles key-value config and small serialized objects: feature flags, sync tokens, user preferences.

The decision framework is simple: if you query, join, or paginate the data, use Room database. If you're storing a handful of scalar values or one small Proto DataStore message, DataStore's transactional edit{} block is the simpler and safer choice.

AndroidX DataStore reached its 1.1.1 stable release on Google Maven in 2024, and the SharedPreferencesMigration API has stayed compatible since, so a migration started today inherits two years of production hardening. If navigating this migration in-house feels daunting, partnering with an expert Android development team can help ensure the transition is handled with production-grade rigor.

Both libraries move I/O off the main thread by default, Room's suspend functions and DataStore's Flow<Preferences> on Dispatchers.IO, which is the exact main-thread blocking risk that SharedPreferences.apply carried under heavy write load.

On corruption, DataStore falls back to a Serializer default automatically; a corrupted Room database usually needs a schema migration. In our migrations, apps ended up running both: Room for anything a user browses, DataStore for anything they configure once.

How does DataStore handle read errors and corrupted data?

DataStore treats every read as fallible by design: Flow<Preferences> throws IOException on disk read failures, and you catch it upstream rather than crashing the collector. Android Developers' DataStore guide documents this pattern directly, catch the IOException, emit an empty Preferences or a defined default, and let the app keep running instead of showing a blank screen.

Corruption is handled separately through corruptionHandler, a parameter on the preferencesDataStore or Proto DataStore delegate. When deserialization fails, a malformed proto blob, a partially written file, the handler runs a replacement strategy you define, typically ReplaceFileCorruptionHandler, which swaps in a default value and logs the event rather than propagating the exception up the stack.

Writes are atomic. DataStore's edit{} transform block runs on Dispatchers.IO, writes to a temp file, then does an atomic rename over the real one, so a crash mid-write never leaves a half-written file for the next read to choke on. That single guarantee is what SharedPreferences never gave you.

One caveat worth flagging early: DataStore is not safe across processes. If your app has a multiprocess architecture (a separate:widget or:sync process), concurrent access to the same DataStore file needs MultiProcessDataStoreFactory, plain preferencesDataStore will silently corrupt under contention.

We hit this during a migration where a background sync service and the main process wrote the same key namespace independently; wrapping both in MultiProcessDataStoreFactory fixed the race without touching call sites.

Using DataStore with Jetpack Compose and testing it

Jetpack Compose reads DataStore with collectAsState, turning Flow<Preferences> into recomposition-safe state without the boilerplate LiveData or RxJava bridging that legacy screens still carry.

val userPrefs by context.dataStore.data.map { it[NAME_KEY] ?: "" }.collectAsState(initial = "")

That single call replaces an Observer registration and manual lifecycle teardown. In our migrations, swapping a LiveData-backed ViewModel for direct collectAsState cut the settings screen's ViewModel down to a thin mapping layer, since Compose already owns the collection lifecycle.

Testing gets simpler too. Instead of mocking SharedPreferences.Editor, we build a fake DataStore<Preferences> backed by an in-memory Preferences map and inject it through the same constructor the app uses.

Google's own DataStore sample repo uses this pattern for its instrumented tests, and it means test setup takes one function instead of a Robolectric shadow. No thread juggling, no flaky writes, and assertions read the exact type the UI consumes.

Pros and cons summary

SharedPreferences and Jetpack DataStore solve the same problem with different guarantees, and the table below is the one we hand to teams mid-migration when they need a quick sanity check.

Aspect SharedPreferences Jetpack DataStore
Thread safety Synchronous reads can block the main thread Built on Kotlin coroutines and Flow, off-thread by default
Data type Simple key-value pairs Preferences DataStore (key-value) or Proto DataStore (typed schema)
Consistency No transactional guarantee; partial writes possible Transactional edit{} block, atomic writes
Error handling Silent failures, corrupted files return defaults Serializer<T> and corruptionHandler let you recover or fall back
Migration path N/A SharedPreferencesMigration API moves existing keys automatically
API shape Callback-based listeners Flow<Preferences> for reactive state

For an app already storing structured objects, Room database remains the better store than either option. DataStore is the direct replacement when SharedPreferences is doing double duty as a lightweight database. If you're weighing local database options beyond Room, our Realm vs ObjectBox comparison breaks down how each handles write transactions.

FAQ: SharedPreferences vs DataStore

Is SharedPreferences deprecated in Android?

SharedPreferences is not formally deprecated in the Android SDK. But Google's own architecture guidance now points new code toward Jetpack DataStore, which reached stable version 1.1.1 per the Google Maven release notes. The old API still ships in every release, with no removal date ever announced. Start new features on DataStore and migrate legacy keys opportunistically rather than in one risky sweep. If your app targets Kotlin Multiplatform Mobile, weigh these Android-specific options against cross-platform key-value storage needs before committing to either. The DataStore SharedPreferences comparison shifts once other platforms enter the picture.

Does DataStore actually outperform SharedPreferences?

DataStore avoids main-thread blocking because every read runs off-thread through Kotlin coroutines and Flow, while SharedPreferences.apply can still queue a synchronous disk write behind the scenes. Raw benchmarks actually favor the old API: SharedPreferences read and write operations finish 1-2ms faster, and the gap widens to roughly 4x for string values. That sounds like a regression, but it isn't the whole story. In our migrations, the visible win was fewer ANRs tied to preference writes, not faster cold starts. A slightly slower update that never blocks the UI thread beats a faster one that occasionally does.

Can I use room and DataStore together?

Yes, Room and DataStore handle different data types and combine cleanly in most apps. Room is built for structured, queryable data such as user records or order history, giving you SQL, joins, and indexes. DataStore is built for small key-value storage: a theme choice, a boolean feature flag, or a last-sync timestamp. The advantages of splitting responsibilities this way go both directions. Room stays fast because it isn't managing tiny scalar settings, and DataStore stays simple because it never has to model relationships between tables. Use Room for anything you'd filter, sort, or join. Use DataStore, whether Preferences DataStore or ProtoDataStore, for everything else, especially settings that benefit from type safety.

Does DataStore support multiple processes safely?

Not by default. A plain preferencesDataStore delegate assumes single-process access and will silently corrupt data if two processes write to it concurrently, for example a :widget or :sync process alongside your main app process. Route both through MultiProcessDataStoreFactory instead; it coordinates access across processes so concurrent writes don't race. If your app is single-process, which covers most Android apps, this doesn't apply and the default delegate is fine.

Plan your DataStore migration with confidence

Start with an audit, not a rewrite. Pull a list of every SharedPreferences file your app writes to, count the keys in each, and flag the ones read on a background thread today, those are your highest-risk migration candidates because they're the ones already fighting the main-thread blocking DataStore exists to remove.

Migrate opportunistically: new features go straight on DataStore, existing keys move over as you touch that code anyway, using the SharedPreferencesMigration API so the transfer stays transactional. Reserve a full-sweep migration for apps where namespace collisions or corrupted-write bugs are already costing you support time.

Refining an existing Android codebase or planning a broader cross-platform strategy both benefit from our mobile development services, which can help you turn a modernized data layer into a stronger overall app foundation.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business