Why does this viewModelScope.launch still freeze the interface?
A screen freezes for several seconds and the system reports an ANR, even though the work runs inside a coroutine launched from the ViewModel.
Constraints: parseJson is a CPU-heavy pure function and db.query() is a blocking driver call. Neither of them is a suspend function, and neither may be rewritten.
fun load() {
viewModelScope.launch {
val rows = db.query("SELECT * FROM items")
val items = parseJson(rows)
_state.value = items
}
}
Diagnose the cause and fix it.
viewModelScope dispatches to Dispatchers.Main, and launch alone does not move work off it. Both calls block rather than suspend, so they hold the UI thread. Wrap the query in withContext(Dispatchers.IO) and the parse in withContext(Dispatchers.Default).
- ✗Assuming any
launchautomatically runs its body off the main thread - ✗Thinking that marking a function
suspendmakes a blocking call non-blocking - ✗Confusing the default dispatcher of
viewModelScopewithDispatchers.Default
- →Why would
withContext(Dispatchers.IO)be the wrong dispatcher forparseJson? - →How do you make a repository function main-safe so its callers need no
withContext?
The cause
viewModelScope is built on Dispatchers.Main.immediate — the body of launch runs on the UI thread. launch creates a coroutine but does not change the dispatcher. A coroutine gives its thread back only at a suspension point, and db.query() and parseJson() are plain blocking calls with no suspension point in them. The UI thread is held throughout — hence the ANR.
fun load() {
viewModelScope.launch { // Dispatchers.Main
val rows = db.query(...) // blocks the UI thread
val items = parseJson(rows) // burns CPU on the UI thread
_state.value = items
}
}
The fix
Move each kind of work onto the dispatcher that suits it, leaving the state write on Main:
fun load() {
viewModelScope.launch {
val rows = withContext(Dispatchers.IO) { db.query("SELECT * FROM items") }
val items = withContext(Dispatchers.Default) { parseJson(rows) }
_state.value = items // back on Main
}
}
withContext creates no new coroutine: it suspends the current one, releasing the UI thread, and returns the result. IO carries the blocking driver call, Default the CPU-bound parse. Better still, hide the withContext inside the repository so callers get main-safe functions.