SharedPreferences vs DataStore: Should you migrate?
Contents
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.
- Add the dependency. Pull the current stable androidx.datastore:datastore-preferences version from Google Maven, not whatever a two-year-old blog post recommends.
- Declare the delegate. Create a preferencesDataStore delegate at file scope, one per logical preferences group, not one giant global store.
- Wire the migration. Pass a SharedPreferencesMigration instance into the DataStore builder, pointing at the legacy SharedPreferences file name.
- Replace reads. Swap getString/getBoolean calls for
Flow<Preferences>collection inside a coroutine on Dispatchers.IO, moving preference reads off the main thread entirely. - 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?
Does DataStore actually outperform SharedPreferences?
Can I use room and DataStore together?
Does DataStore support multiple processes safely?
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.
