Два запроса к серверу, каждый по секунде. Экран грузится две секунды. Вы точно помните, что корутины нужны как раз для того, чтобы так не было:
viewModelScope.launch {
val user = api.getUser() // 1 секунда
val feed = api.getFeed() // ещё одна
render(user, feed)
}
Корутина здесь ни при чём: вы написали последовательный код. Он не блокирует главный поток — и на этом его заслуги заканчиваются. Чтобы запросы пошли одновременно, нужен другой билдер.
Три билдера и чем они отличаются
Билдер — это функция, которая создаёт корутину и запускает её. Их три, и путаница между ними порождает больше проблем, чем любая другая тема в корутинах.
| Что возвращает | Блокирует поток | Когда нужен | |
|---|---|---|---|
launch | Job | нет | действие без результата |
async | Deferred<T> | нет | нужен результат |
runBlocking | результат блока | да | мост из обычного кода |
launch — выстрелил и забыл
launch запускает работу и не возвращает результата. Он отдаёт Job — ручку, которой можно отменить корутину или дождаться её через join().
Это рабочая лошадка: сохранить, отправить, залогировать, обновить экран. Если результат не нужен прямо в коде — берите launch.
async — когда нужен результат
async возвращает Deferred<T> — обещание результата. Забрать его можно вызовом await(). Именно так чинится пример из начала статьи:
viewModelScope.launch {
val user = async { api.getUser() }
val feed = async { api.getFeed() }
render(user.await(), feed.await()) // ~1 секунда вместо двух
}
Обе корутины стартуют сразу, ждут параллельно, и общее время равно максимуму из двух, а не сумме.
А вот так делать бессмысленно — это снова последовательный код, только с лишними буквами:
val user = async { api.getUser() }.await() // сразу ждём
val feed = async { api.getFeed() }.await() // и только потом запускаем вторую
Правило простое: async имеет смысл, только если между запуском и await() вы успеваете запустить что-то ещё.
Вторая особенность async — исключения. У launch ошибка улетает вверх по дереву корутин сразу, у async она сохраняется внутри Deferred и выбрасывается при await(). Поэтому и ловить её нужно вокруг await().
runBlocking — мост, а не билдер на каждый день
runBlocking запускает корутину и блокирует текущий поток, пока та не закончит. То есть делает ровно то, от чего корутины должны избавлять.
Законных применений у него три:
- мост из обычного блокирующего кода в корутинный — например, из старого легаси;
- обёртка вокруг
main(), чтобы программа не завершилась раньше корутин; - тесты.
Причём для второго и третьего давно есть замена: main() с Kotlin 1.3 можно объявить как suspend, а для тестов существует runTest с виртуальным временем.
В Android-коде runBlocking почти всегда ошибка. Если он оказался в главном потоке — интерфейс замирает, и приложение получает ANR.
Что обычно спрашивают на собеседовании
«Чем launch отличается от async» — вопрос-разминка, но ответ «один возвращает Job, другой Deferred» засчитывается наполовину. Хороший ответ добавляет про исключения: у launch они уходят вверх немедленно, у async ждут await(). И про то, что параллельность даёт не сам async, а то, что вы не вызываете await() сразу.
Подробнее про дерево корутин и то, кто кого ждёт, — в руководстве по корутинам.