Почему UI всё равно подвисает, хотя в репозитории добавили flowOn(Dispatchers.IO)?
Репозиторий отдаёт flow статей и уводит его с главного потока через flowOn(Dispatchers.IO). Экран всё равно теряет кадры — тяжёлый разбор JSON явно идёт на главном потоке, и профайлер показывает его внутри блока collect.
Ограничение: parseHeavy должен уйти с главного потока, а обновление UI — остаться на нём. Не запускайте вторую неструктурированную корутину.
// репозиторий
fun articles(): Flow<List<Article>> = flow { emit(api.load()) }
.flowOn(Dispatchers.IO)
// view model, сбор идёт с главного диспетчера
viewModelScope.launch {
repo.articles().collect { raw ->
val parsed = parseHeavy(raw) // 300 мс работы CPU
render(parsed)
}
}
Найдите и исправьте ошибку.
flowOn меняет контекст только для операторов ВЫШЕ по цепочке — для производителя и всего, что объявлено до вызова. Коллектор всегда работает в контексте той корутины, которая вызвала collect, потому что Flow сохраняет контекст. Поэтому parseHeavy внутри collect остаётся на главном потоке; перенесите его выше flowOn.
- ✗Считать, что
flowOnуводитcollectс вызывающего диспетчера - ✗Думать, что
flowOnдействует на операторы, стоящие после него в цепочке - ✗Делать тяжёлую работу CPU внутри блока collect на главном диспетчере
- →Что означает сохранение контекста для
Flowи зачем оно навязано? - →Что сделают два вызова
flowOnв одной цепочке с операторами между ними?
Почему так
Flow даёт гарантию context preservation: эмиссия обязана происходить в том же контексте, в котором её видит коллектор. Единственный легальный способ сменить контекст — flowOn, и он меняет его только для upstream: производителя и операторов, объявленных до вызова.
Всё, что стоит ниже — включая терминальный collect, — исполняется в контексте корутины, которая вызвала collect. Здесь это viewModelScope, то есть Dispatchers.Main.
fun articles(): Flow<List<Article>> = flow { emit(api.load()) }
.map { parseHeavy(it) } // upstream → уедет на IO
.flowOn(Dispatchers.IO) // граница: влияет только на то, что ВЫШЕ
viewModelScope.launch {
repo.articles().collect { parsed -> render(parsed) } // Main: только рендер
}
Альтернатива, если разбор обязан остаться на стороне коллектора:
repo.articles().collect { raw ->
val parsed = withContext(Dispatchers.Default) { parseHeavy(raw) }
render(parsed)
}