Engineering

Choosing Between SharedPreferences and Jetpack DataStore

A deep dive into Android data persistence, comparing legacy SharedPreferences with modern DataStore based on our experience building AIA apps.

Amin Shekh · October 9, 2026
Share

When you are building a focused utility app, the architectural choices for how you save a single boolean or a user-selected calculation method can have long-term impacts on app stability. At AIA Innovations, we focus on tools that work reliably without a cloud backend, which means our local storage strategy is the backbone of the user experience. Whether it is saving the preferred calculation angle in Salah Companion or storing the preferred screening standard in Shariah Signal, we often face a choice between the legacy SharedPreferences API and the modern Jetpack DataStore.

For years, SharedPreferences was the default. It was simple, accessible from anywhere, and required very little boilerplate. However, as Android has evolved toward reactive programming and stricter main-thread safety, the cracks in SharedPreferences have become harder to ignore. Choosing the wrong mechanism can lead to Application Not Responding (ANR) errors or subtle data inconsistencies that are difficult to debug in production.

The Technical Reality of SharedPreferences

SharedPreferences is a synchronous API. When you call getString or getBoolean, the system reads from an XML file. If the file hasn't been loaded into memory yet, the call blocks the thread until the disk I/O is complete. If this happens on the UI thread, you risk a stuttering interface or an outright crash. While apply() allows for asynchronous writes, it lacks a mechanism for error handling. If the write fails, your app simply proceeds as if it succeeded.

We encountered this limitation early on when managing complex settings like high-frequency prayer reminders. Because SharedPreferences provides no native support for handling I/O errors, a corrupted XML file could lead to default values being loaded without the user’s knowledge, potentially causing missed notifications. This led us to explore Reliable Android Background Tasks and, eventually, a better way to persist the settings that trigger those tasks.

Enter Jetpack DataStore

Jetpack DataStore was designed to address the specific shortcomings of SharedPreferences. It is built on top of Kotlin Coroutines and Flow, making it inherently asynchronous and safe to call from the main thread. It comes in two flavors:

  1. 1.Preferences DataStore: Like SharedPreferences, this uses key-value pairs but handles data updates transactionally.
  2. 2.Proto DataStore: This uses Protocol Buffers to store typed objects, providing full type safety and schema definitions.

In our studio, we generally lean toward Preferences DataStore for simple flags and Proto DataStore for complex configurations, such as the multi-factor screening criteria used in Shariah Signal.

Comparison Table: Performance and Safety

FeatureSharedPreferencesJetpack DataStore
ThreadingSynchronous (blocks UI thread)Asynchronous (Coroutines/Flow)
Error HandlingNone (silent failures)Built-in (catchable exceptions)
ConsistencyWeak (no transactional API)Strong (ACID guarantees)
Type SafetyNo (runtime cast errors)Yes (with Proto DataStore)
Migration SupportManualBuilt-in Migration API

When to Stick with SharedPreferences

Despite being labeled as "legacy," SharedPreferences still has a place in specific scenarios. If you are maintaining a legacy codebase that is heavily reliant on synchronous calls and does not yet use Kotlin Coroutines, the cost of refactoring might outweigh the benefits. Furthermore, for extremely small, non-critical flags where a 10ms UI block is statistically insignificant and you don't mind the lack of error reporting, SharedPreferences is functional.

However, for new projects like our in-development Employ Ready app, we no longer use SharedPreferences. The overhead of setting up DataStore is now minimal compared to the long-term benefit of having a stream of data updates that reactively change the UI.

The Case for DataStore in Reactive UI

A major advantage of DataStore is its integration with Kotlin Flow. In Salah Companion, the UI needs to update instantly if a user changes their location or calculation method. With SharedPreferences, you would need to register a OnSharedPreferenceChangeListener and manually trigger UI refreshes, often leading to "spaghetti code" where the UI state and the disk state fall out of sync.

With DataStore, the UI simply collects a Flow. The moment the data on disk changes, the Flow emits a new value, and the UI updates automatically. This architecture ensures that the user is always looking at the truth. This is particularly vital when building apps that follow Offline-First Design principles, where the local storage is the primary source of truth for the entire application.

Practical Example: Storing a Calculation Method

Let’s look at a concrete example from our development of Shariah Signal. We need to store a user's preferred screening standard (e.g., AAOIFI vs. DJIM).

``kotlin val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) // Saving prefs.edit().putString("screening_standard", "AAOIFI").apply() // Loading val standard = prefs.getString("screening_standard", "DJIM") ` This looks simple, but the loading is blocking. If the file is large, the getString` call will hold up the main thread.

The DataStore Way

```kotlin val STANDARD_KEY = stringPreferencesKey("screening_standard")

val standardFlow: Flow<String> = context.dataStore.data .map { preferences -> preferences[STANDARD_KEY] ?: "DJIM" }

// Updating suspend fun updateStandard(newStandard: String) { context.dataStore.edit { settings -> settings[STANDARD_KEY] = newStandard } } `` While the code is slightly more verbose, it is safer. The edit block is a transaction; if the phone loses power during the write, the file won't be left in a corrupted state. The standardFlow can be collected directly in the ViewModel using collectAsStateWithLifecycle` in Jetpack Compose, creating a seamless reactive loop.

Handling Migrations

one of the biggest hurdles in moving from SharedPreferences to DataStore is the existing user data. You cannot simply flip a switch and lose user settings. Fortunately, Google provides a SharedPreferencesMigration object. When we updated our internal tools, we used this to automatically move data to the new format the first time the app launched after the update. The migration only runs once and deletes the old SharedPreferences file once the data is safely moved to the DataStore.

Common Pitfalls to Avoid

  1. 1.Accessing DataStore on the Main Thread: While DataStore is safe to declare on the main thread, collecting the Flow or performing an edit should always happen within a Coroutine scope.
  2. 2.Creating Multiple Instances: DataStore should be a singleton. In our apps, we typically delegate the creation to a top-level property or use a Dependency Injection framework like Hilt to ensure only one instance of the DataStore exists for a specific file.
  3. 3.Over-Engineering: If you only have two boolean flags, Proto DataStore (which requires defining a .proto file) is likely overkill. Stick to Preferences DataStore unless you need complex data types or have a high number of related settings.

Strategic Recommendation

For most independent studios, the decision should be based on the complexity of the app's lifecycle. If your app is built using modern Jetpack libraries (Compose, ViewModel, Flow), DataStore is the clear winner. It aligns with the reactive nature of modern Android. However, if you are maintaining a very old app with a limited budget for refactoring, SharedPreferences is "fine" as long as you are aware of its blocking nature and handle I/O exceptions manually where possible.

At AIA Innovations, we have moved all active development to DataStore. The peace of mind that comes from knowing a settings change won't cause a random ANR is worth the slightly steeper learning curve. It allows us to focus on the core functionality of our apps—like ensuring prayer times are accurate or financial data is clear—without worrying about the plumbing of the Android OS.

What to do next

  1. 1.Audit your current storage: Open your res/xml folder and look for PreferencesScreen or search your codebase for getSharedPreferences. Identify which ones are critical for app startup.
  2. 2.Implement a Pilot Migration: Choose a non-critical feature (like a "Dark Mode" toggle) and migrate it to Preferences DataStore using the built-in migration API to get a feel for the workflow.
  3. 3.Standardize your Singleton: Ensure that your DataStore instance is defined only once, preferably in a dedicated DataStoreModule or as a top-level extension property, to avoid IllegalStateException errors.
#android development#jetpack datastore#kotlin#mobile engineering

One email per launch. No spam, unsubscribe any time.