runBlocking
is a coroutine builder that blocks the current thread until your coroutine finishes. While it seems convenient, it can cause serious problems, especially on Android’s main thread.
How runBlocking Works
Here’s what happens inside runBlocking
:
public fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
val currentThread = Thread.currentThread()
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
if (contextInterceptor == null) {
// Set up a private or thread-local event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
// Reuse an existing event loop or context interceptor
eventLoop = (contextInterceptor as? EventLoop)
?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
// Launch a blocking coroutine
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
The key point: runBlocking
blocks the thread it runs on until your coroutine finishes. This means no other work can happen on that thread while it waits.
Why It’s Dangerous on Android
UI Freezes & ANRs
- Android’s main thread handles UI updates and user interactions
- Blocking it causes UI freezes
- Long blocks can trigger ANR (Application Not Responding) crashes
The Problem with Convenience
- It might seem easy to use
runBlocking
in lifecycle methods - But the problems it causes (freezes, ANRs) are much worse than the convenience it provides
When to Use runBlocking
1. Unit and Integration Tests
@Test
fun `when fetching data then returns success`() = runTest {
val result = repository.fetchData()
assertTrue(result.isSuccess)
}
Better: Use runTest
instead of runBlocking
- it skips delays and makes tests faster.
2. Background Worker Threads
thread {
runBlocking {
doLongRunningWork()
}
}
Better: Use structured coroutines with job.join()
instead.
Best Practices & Alternatives
Scenario | Don’t Use | Use Instead |
---|---|---|
Android UI code | runBlocking { /*…*/ } | lifecycleScope.launch { /*…*/ } |
Waiting on a job | runBlocking | job.join() |
Unit testing | runBlocking | runTest |
Background work | runBlocking | Structured coroutine scopes |
Key Takeaways
-
Keep Code Non-Blocking
- Use
launch
,async
, and lifecycle-aware scopes - Avoid blocking the main thread
- Use
-
Use Structured Concurrency
- Manage coroutine lifecycles properly
- Avoid orphaned tasks
-
Test Efficiently
- Use
runTest
instead ofrunBlocking
- Skip real delays in tests
- Use
Conclusion
runBlocking
is not a good solution for Android apps. It blocks threads and can cause serious problems. Instead, use proper coroutine scopes and builders to keep your app responsive and reliable.
Related Articles
To learn more about coroutines and Android development:
- Kotlin Coroutines: Dispatchers - Learn about different dispatchers and when to use them
- SupervisorScope vs viewModelScope - Understanding different coroutine scopes
- Kotlin Coroutines: Channels - Learn about coroutine communication
- UI Layer with UDF Pattern - Implementing clean architecture with coroutines
References
- Exercise Caution When Using runBlocking on Android - ProAndroidDev