Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt

Use this file to discover all available pages before exploring further.

SharedPreferences has three distinct ANR vectors that catch many Android developers off guard: the first getSharedPreferences() call performs disk I/O, commit() is always a synchronous disk write, and apply() flushes synchronously during Activity stop via QueuedWork.waitToFinish().

The Three ANR Vectors

1. getSharedPreferences() performs disk I/O on the first call

The first time a given preferences file is opened, Android reads the XML file from disk into memory. On slow or busy storage this can take 50–200 ms. If that call happens on the main thread — inside a lifecycle callback or onViewCreated — it blocks the UI pipeline for the full duration.
// ❌ BAD: SharedPreferences disk I/O on Main
class SettingsFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val pref = requireContext()
            .getSharedPreferences("user_prefs", Context.MODE_PRIVATE)  // disk read on Main
            .getString("token", null)
        render(pref)
    }
}

2. commit() is always a synchronous disk write

commit() writes to disk synchronously on whatever thread you call it from. Calling it on the main thread is a direct ANR risk. It will also be flagged immediately by StrictMode. Always use apply() instead — with the caveats described below.
// ❌ BAD: commit() is a synchronous disk write — blocks Main
prefs.edit().putString("key", value).commit()

3. apply() flushes synchronously during Activity stop

apply() is asynchronous for the write itself, but it places a pending write into QueuedWork. Android flushes QueuedWork synchronously on the main thread inside handleStopActivity and handleServiceArgs. On slow storage or under memory pressure, this flush can take 200–500 ms and cause an ANR that shows up as waiting in framework code rather than in your own stack.
// ANR trace signature for apply() flush:
"main" prio=5 tid=1 Waiting
  at java.lang.Object.wait(Native method)
  at com.android.internal.util.QueuedWork.waitToFinish(QueuedWork.java:...)
  at android.app.ActivityThread.handleStopActivity(ActivityThread.java:...)

The Correct Pattern

Read preferences on Dispatchers.IO, cache the result in memory, and write with apply(). This eliminates the first-call disk I/O from the main thread and removes synchronous write risk.
// ✅ CORRECT: read on IO; cache in memory; write with apply()
class TokenRepository @Inject constructor(
    @ApplicationContext context: Context
) {
    private val prefs by lazy(LazyThreadSafetyMode.NONE) {
        context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
    }

    suspend fun readToken(): String? = withContext(Dispatchers.IO) {
        prefs.getString("token", null)
    }

    fun writeToken(token: String) {
        prefs.edit().putString("token", token).apply()  // async; never use commit()
    }
}

The Better Solution: Migrate to DataStore

DataStore is the modern replacement for SharedPreferences. It is fully coroutine-based, never performs synchronous disk writes, and has no QueuedWork flush at Activity stop. For any new code, start with DataStore. For existing code, migrate incrementally starting with the preferences files that are accessed most frequently on the main thread.
DataStore exposes preferences as a Flow, making reads reactive and writes explicit suspension functions. There is no commit vs apply distinction and no silent synchronous flush.
// ✅ BETTER: migrate to DataStore (fully coroutine-based, no synchronous writes)
val Context.userPrefsStore by preferencesDataStore("user_prefs")

Full DataStore repository example

// ✅ BEST: migrate to DataStore — fully coroutine-based, no synchronous QueuedWork flush
val Context.settingsStore by dataStore("settings", SettingsSerializer)

class SettingsRepository(private val store: DataStore<AppSettings>) {
    val settings: Flow<AppSettings> = store.data
    suspend fun save(settings: AppSettings) { store.updateData { settings } }
}

ANR Risk Summary

APIThread behaviorANR risk
getSharedPreferences() (first call)Synchronous disk read on calling threadHigh if called on Main
getSharedPreferences() (subsequent calls)Returns in-memory cacheNone
commit()Synchronous disk write on calling threadHigh on Main
apply()Asynchronous write, but flushes at Activity stopMedium — depends on storage speed and write volume
DataStore store.dataNon-blocking FlowNone
DataStore store.updateDataSuspending coroutine on IONone

Build docs developers (and LLMs) love