Корутины в Kotlin: практическое введение
Практика3 минРедакция Innovialabs
Коротко
Корутины в Kotlin позволяют писать асинхронный код последовательно: suspend-функции не блокируют поток, launch и async запускают работу в scope, а отмена идёт по иерархии автоматически.
Что такое корутины и зачем они нужны
Корутина — это вычисление, которое может приостановиться и продолжиться позже, не блокируя поток. В Kotlin это позволяет писать сетевые запросы и работу с базой как обычный последовательный код, без колбэков:
suspend fun loadProfile(id: String): Profile {
val user = api.getUser(id) // приостановка, поток свободен
val orders = api.getOrders(id)
return Profile(user, orders)
}
Корутины живут в библиотеке kotlinx.coroutines. На Android главный поток при этом не зависает, а интерфейс остаётся отзывчивым.
suspend-функции
Модификатор suspend означает: функция может приостановиться. Вызвать её можно только из другой suspend-функции или из корутины.
Важно понимать:
suspendне делает функцию фоновой сама по себе. Тяжёлую работу всё равно нужно перенести на подходящий диспетчер.- Хорошая suspend-функция main-safe: её можно безопасно вызвать из главного потока, потому что она сама переключает контекст.
suspend fun readFile(path: String): String =
withContext(Dispatchers.IO) {
File(path).readText()
}
launch или async
launch | async | |
|---|---|---|
| Возвращает | Job | Deferred<T> |
| Результат | Не нужен | Получается через await() |
| Когда применять | «Сделай и забудь»: сохранить, отправить | Параллельные запросы с результатом |
Параллельная загрузка двух ресурсов:
suspend fun loadScreen(id: String) = coroutineScope {
val user = async { api.getUser(id) }
val orders = async { api.getOrders(id) }
Screen(user.await(), orders.await())
}
Если один из запросов упадёт, второй будет отменён, а ошибка дойдёт до вызывающего кода.
Диспетчеры: где выполняется код
- Dispatchers.Main — главный поток UI (Android, desktop).
- Dispatchers.IO — сеть, файлы, база данных: блокирующие операции ввода-вывода.
- Dispatchers.Default — вычисления, нагружающие процессор: парсинг, сортировка, обработка изображений.
Переключайтесь через withContext, а не через запуск новых корутин. Многие сетевые библиотеки, например Retrofit, сами предоставляют suspend-функции, и оборачивать их вызов в Dispatchers.IO не требуется.
Структурированная конкурентность
Каждая корутина запускается внутри CoroutineScope, и это главный принцип:
- Родитель ждёт завершения всех детей.
- Ошибка ребёнка отменяет родителя и остальных детей.
- Отмена scope отменяет всё внутри.
На Android используйте готовые scope: viewModelScope во ViewModel и lifecycleScope в Activity и Fragment. Они отменяются вместе с компонентом, поэтому запросы не продолжают работу после закрытия экрана.
class ProfileViewModel : ViewModel() {
fun load(id: String) {
viewModelScope.launch {
try {
_state.value = UiState.Data(repo.loadProfile(id))
} catch (e: IOException) {
_state.value = UiState.Error
}
}
}
}
Избегайте GlobalScope: корутины в нём не привязаны к жизненному циклу и легко утекают.
Отмена
Отмена в корутинах кооперативная: код должен в ней участвовать.
- Все suspend-функции из kotlinx.coroutines (
delay,withContextи другие) проверяют отмену сами. - В долгих циклах вызывайте
ensureActive()или проверяйтеisActive. - Не глотайте
CancellationExceptionв общемcatch (e: Exception)— пробрасывайте её дальше. - Для освобождения ресурсов используйте
finally, а если там нужно вызвать suspend-функцию —withContext(NonCancellable).
Таймаут на сетевой запрос:
val result = withTimeoutOrNull(5_000) { api.getUser(id) }
Частые ошибки
- runBlocking в продакшен-коде блокирует поток — он нужен в
mainи тестах. - Запуск корутины без scope или в
GlobalScope. - Тяжёлые вычисления на Dispatchers.Main, из-за которых интерфейс подвисает.
- async без await: ошибка может потеряться или всплыть неожиданно.