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?
2. Which StrictMode policy should you enable to detect network calls on the main thread?
3. Why is SharedPreferences.apply() potentially problematic for ANR?
4. Which dispatcher should be used in a coroutine for a network call?
Flashcards
Question
What is an ANR in Android?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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.