Почему этот viewModelScope.launch всё равно замораживает интерфейс?
Экран замирает на несколько секунд, система рапортует ANR, хотя работа идёт внутри корутины, запущенной из ViewModel.
Ограничения: parseJson — тяжёлая по CPU чистая функция, а db.query() — блокирующий вызов драйвера. Ни одна из них не suspend, и переписывать их нельзя.
fun load() {
viewModelScope.launch {
val rows = db.query("SELECT * FROM items")
val items = parseJson(rows)
_state.value = items
}
}
Определите причину и исправьте.
viewModelScope диспетчеризует на Dispatchers.Main, а launch сам работу оттуда не уносит. Оба вызова блокирующие, а не приостанавливаемые, поэтому удерживают UI-поток. Оберните запрос в withContext(Dispatchers.IO), а разбор — в withContext(Dispatchers.Default).
- ✗Считать, что любой
launchавтоматически уносит тело с главного потока - ✗Думать, что пометка функции как
suspendделает блокирующий вызов неблокирующим - ✗Путать диспетчер
viewModelScopeпо умолчанию сDispatchers.Default
- →Почему
withContext(Dispatchers.IO)был бы неверным диспетчером дляparseJson? - →Как сделать функцию репозитория безопасной для
Main, чтобы вызывающим не нужен былwithContext?
Причина
viewModelScope построен на Dispatchers.Main.immediate — тело launch выполняется на UI-потоке. launch создаёт корутину, но не меняет диспетчер. Корутина отдаёт поток только в точке приостановки, а db.query() и parseJson() — обычные блокирующие вызовы, точек приостановки в них нет. UI-поток удерживается всё это время — отсюда ANR.
fun load() {
viewModelScope.launch { // Dispatchers.Main
val rows = db.query(...) // блокирует UI-поток
val items = parseJson(rows) // жжёт CPU на UI-потоке
_state.value = items
}
}
Исправление
Уносим каждый вид работы на свой диспетчер, оставляя запись состояния на 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 // снова на Main
}
}
withContext не создаёт новую корутину: он приостанавливает текущую, освобождая UI-поток, и возвращает результат. IO — под блокирующий драйвер, Default — под CPU-разбор. Лучше вообще спрятать withContext внутрь репозитория, чтобы вызывающие получали main-safe функции.