Skip to content
advanced Phase 11 · Performance & Optimization

ANR Prevention

Avoid Application Not Responding by moving work off the main thread and understanding strict mode.

40m
2 problems
Topic Progress 0%

What Causes ANR?

What is an ANR?

ANR stands for Application Not Responding. When your app's main thread is blocked for too long, Android displays an ANR dialog asking the user to wait or kill the app. This is one of the most damaging UX issues because it directly tells the user your app is broken.

ANR Timeout Thresholds

Android has different timeout thresholds for different components:

Component Timeout
InputEvent (touch, key) 5 seconds
BroadcastReceiver.onReceive() 10 seconds (foreground) / 60 seconds (background)
Service lifecycle callbacks 20 seconds
ContentProvider.onCreate() 10 seconds

If any of these callbacks do not return within the threshold, the system triggers an ANR.

The Main Thread Rule

The main thread (UI thread) handles all user input, view drawing, and lifecycle callbacks. It must never be blocked. The rule is simple: if an operation takes more than a few milliseconds, do not run it on the main thread.

// ANR: Network call on main thread
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // This blocks the main thread
        val response = URL("https://api.example.com/data").readText()
        textView.text = response
    }
}

This blocks the main thread until the network response arrives. On a slow connection, this will trigger an ANR.

What Operations Block the Main Thread?

Network operations: HTTP requests, DNS lookups, socket connections
Disk I/O: File reads/writes, SharedPreferences.commit(), SQLite queries
CPU-intensive work: JSON parsing, image processing, complex calculations
Synchronization: Lock contention, waiting on other threads
IPC: Binder transactions, ContentProvider queries

StrictMode Detection

StrictMode is a debug tool that detects accidental disk or network access on the main thread:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()
                    .penaltyLog()
                    .penaltyFlashScreen()
                    .build()
            )
            StrictMode.setVmPolicy(
                StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .detectLeakedClosableObjects()
                    .detectActivityLeaks()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

StrictMode logs violations to logcat with a stack trace showing exactly where the blocking call happened. Use it in debug builds to catch these issues early.

Reading ANR Traces

When an ANR occurs, the system writes a trace file to /data/anr/traces.txt. You can pull it with adb pull /data/anr/traces.txt. The trace shows:

  • The main thread's current stack trace
  • What thread it is waiting on (if blocked by a lock)
  • All other threads in the process

Look for the main thread in the trace. If it shows a blocking call like SharedPreferencesImpl.awaitLoaded() or BinderProxy.transactNative(), you have found the culprit.

Preventing ANR with Modern Android

Coroutines for Non-Blocking Work

Kotlin coroutines are the standard way to move work off the main thread:

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                // Switches to IO dispatcher for network call
                val data = withContext(Dispatchers.IO) {
                    api.fetchData()
                }
                _uiState.value = UiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
}

Dispatchers.IO is optimized for blocking I/O operations. Dispatchers.Default is for CPU-intensive work. Dispatchers.Main is for UI updates. Never call Dispatchers.Main for blocking operations.

SharedPreferences vs DataStore

SharedPreferences.commit() is synchronous and blocks the main thread. apply() is asynchronous but can cause ANRs during Activity/Service shutdown because pending writes may not complete.

DataStore replaces SharedPreferences entirely:

// Proto DataStore
data class UserSettings(
    val darkMode: Boolean = false,
    val fontSize: Int = 16
)

val Context.settingsDataStore by dataStore(
    name = "settings",
    serializer = UserSettingsSerializer
)

// Reading (non-blocking)
lifecycleScope.launch {
    settingsDataStore.data
        .map { it.darkMode }
        .collect { isDark ->
            applyTheme(isDark)
        }
}

// Writing (non-blocking)
lifecycleScope.launch {
    settingsDataStore.updateData { current ->
        current.copy(darkMode = !current.darkMode)
    }
}

DataStore uses coroutines and eliminates the ANR risk entirely.

WorkManager for Guaranteed Background Work

WorkManager handles work that must complete even if the app exits:

class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        return try {
            val data = api.syncData()
            database.insertAll(data)
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

// Scheduling
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

WorkManager runs on a background thread and respects system constraints. It is the correct replacement for Services, AlarmManager, and JobScheduler in most cases.

BroadcastReceiver Restrictions

Since Android 8.0, implicit broadcast receivers declared in the manifest are restricted. Most broadcasts must be registered dynamically:

class MyActivity : AppCompatActivity() {
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            // This runs on the main thread
            // Keep it fast
            updateUI()
        }
    }
    
    override fun onStart() {
        super.onStart()
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        registerReceiver(receiver, filter)
    }
    
    override fun onStop() {
        super.onStop()
        unregisterReceiver(receiver)
    }
}

Keep onReceive() implementations short. If you need to do heavy work, start a coroutine or WorkManager task from the receiver.

ContentProvider Restrictions

ContentProvider.onCreate() runs on the main thread during app startup. Lazy initialization is critical:

class MyContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        // Do NOT do heavy initialization here
        // Use lazy initialization or a background thread
        return true
    }
    
    override fun query(...): Cursor? {
        // Initialize database lazily on first query
        if (!::database.isInitialized) {
            database = AppDatabase.getInstance(context!!)
        }
        return database.query(...)
    }
}

Quiz

1. What is the default ANR timeout for a BroadcastReceiver's onReceive() method in the foreground?

Question 1 options

2. Which StrictMode policy should you enable to detect network calls on the main thread?

Question 2 options

3. Why is SharedPreferences.apply() potentially problematic for ANR?

Question 3 options

4. Which dispatcher should be used in a coroutine for a network call?

Question 4 options

Flashcards

Question

What is an ANR in Android?

Answer

Application Not Responding. Android displays this dialog when the main thread is blocked for too long (5s for input, 10s for BroadcastReceiver, 20s for Service).

Question

What is StrictMode and when should you use it?

Answer

StrictMode is a debug tool that detects accidental disk/network access on the main thread. Use it in debug builds to catch ANR-causing code early.

Question

Why is SharedPreferences.apply() an ANR risk?

Answer

apply() queues writes asynchronously. During Activity/Service shutdown, the system waits for pending writes to finish, which can trigger an ANR if disk I/O is slow.

Question

What replaces SharedPreferences for safe, non-blocking preferences?

Answer

Jetpack DataStore (Proto DataStore or Preferences DataStore) uses coroutines and eliminates ANR risk entirely.

Revision Notes

Key Takeaways

  • 1. ANR occurs when the main thread is blocked beyond the timeout threshold
  • 2. StrictMode detects blocking operations in debug builds
  • 3. Use coroutines with Dispatchers.IO for network and disk operations
  • 4. DataStore replaces SharedPreferences and eliminates ANR risk
  • 5. WorkManager is the standard for guaranteed background execution

Interview Tips

  • Explain what ANR stands for and the timeout thresholds for different components
  • Describe how you would use StrictMode to detect ANR-causing code
  • Discuss the difference between SharedPreferences.apply() and commit()
  • Explain when to use WorkManager vs coroutines vs services
  • Know how to read ANR traces to identify the blocking operation

Cheat Sheet

ANR Prevention Cheat Sheet

ANR Timeouts:

  • Input events: 5 seconds
  • BroadcastReceiver: 10s foreground / 60s background
  • Service: 20 seconds
  • ContentProvider: 10 seconds

What blocks the main thread:

  • Network calls
  • Disk I/O (files, SharedPreferences.commit(), SQLite)
  • CPU-intensive work
  • Lock contention
  • Binder transactions

Detection:

  • StrictMode: .detectAll().penaltyLog()
  • ANR traces: adb pull /data/anr/traces.txt
  • Android Studio Profiler

Solutions:

  • Coroutines with Dispatchers.IO or Dispatchers.Default
  • DataStore instead of SharedPreferences
  • WorkManager for guaranteed background work
  • Lazy initialization for ContentProviders
  • Keep BroadcastReceiver.onReceive() short

Rule: If it takes more than a few milliseconds, it does not run on the main thread.