Корутины Kotlin на практике: от азов до продвинутых сценариев

Корутины Kotlin на практике: от азов до продвинутых сценариев

Если вы пишете на Kotlin для Android или backend, вы наверняка слышали о корутинах — легковесных потоках, которые революционизируют асинхронное программирование. Но теория теорией, а настоящая магия раскрывается в примерах. В этой статье мы не просто расскажем, что такое корутины, а погрузимся в живые, рабочие сценарии их использования, которые вы сможете применить в своих проектах уже сегодня.

Что такое корутины и зачем они нужны?

Корутины — это не потоки и не параллельные вычисления в чистом виде. Это более высокоуровневая концепция для упрощения асинхронного и неблокирующего кода. Представьте, что вы можете приостановить выполнение функции, не блокируя поток, а потом возобновить её, когда данные будут готовы. Это и есть корутины.

Ключевое отличие от потоков: корутины очень дешёвые. Вы можете запустить тысячи корутин, тогда как тысячи потоков "съедят" всю память.

Базовые примеры использования

1. Замена AsyncTask и Callback'ов на Android

Раньше для сетевого запроса в Android нужно было писать громоздкий AsyncTask или использовать колбэки, приводящие к "callback hell". С корутинами всё становится элегантным:

// Раньше: колбэки, вложенность, сложная обработка ошибок
// Сейчас:
suspend fun loadUserData(userId: String): User {
    return withContext(Dispatchers.IO) {
        // Симулируем сетевой запрос
        delay(1000)
        User(id = userId, name = "Иван Иванов")
    }
}

// В Activity/Fragment/ViewModel:
lifecycleScope.launch {
    val user = loadUserData("123")
    textView.text = user.name // UI обновляется в главном потоке автоматически!
}

2. Параллельное выполнение запросов

Часто нужно загрузить несколько независимых данных одновременно. С корутинами это делается через async/await:

suspend fun loadDashboardData(): DashboardData {
    val deferredUser = async { userRepository.getUser() }
    val deferredPosts = async { postsRepository.getLatestPosts() }
    val deferredNotifications = async { notificationsRepository.getUnreadCount() }
    
    // Все три запроса выполняются параллельно!
    return DashboardData(
        user = deferredUser.await(),
        posts = deferredPosts.await(),
        notificationCount = deferredNotifications.await()
    )
}

Продвинутые сценарии

3. Ограничение одновременных операций

Что если вам нужно обработать 1000 элементов, но не более 10 одновременно? Используем Semaphore или каналы:

val semaphore = Semaphore(10) // Максимум 10 одновременных корутин

fun processItems(items: List) {
    CoroutineScope(Dispatchers.IO).launch {
        items.map { item ->
            async {
                semaphore.withPermit {
                    processItem(item) // Только 10 корутин одновременно
                }
            }
        }.awaitAll()
    }
}

4. Таймауты и отмена операций

Корутины предоставляют встроенные механизмы для контроля времени выполнения:

lifecycleScope.launch {
    try {
        val result = withTimeout(5000) { // Таймаут 5 секунд
            performNetworkRequest()
        }
        showResult(result)
    } catch (e: TimeoutCancellationException) {
        showError("Запрос занял слишком много времени")
    }
}

Отмена корутины — кооперативная. Функция должна быть suspend и проверять isActive или вызывать другие suspend-функции, которые сами проверяют отмену.

5. Работа с Flow для потоков данных

Для реактивного программирования Kotlin предлагает Flow — аналог RxJava, но проще и нативнее:

fun getLiveSensorData(): Flow = flow {
    while (true) {
        val data = readSensor() // Читаем данные с датчика
        emit(data) // Отправляем подписчику
        delay(1000) // Ждём секунду
    }
}

// Подписываемся:
lifecycleScope.launch {
    getLiveSensorData()
        .filter { it.value > threshold }
        .collect { data ->
            updateUI(data)
        }
}

Распространённые ошибки и лучшие практики

  • Не забывайте про Dispatchers: По умолчанию корутина запускается в контексте родителя. Для IO-операций явно указывайте Dispatchers.IO.
  • Избегайте GlobalScope: Используйте скоупы, привязанные к жизненному циклу (в Android — lifecycleScope, viewModelScope).
  • Обрабатывайте исключения: Используйте try/catch или CoroutineExceptionHandler.
  • Не блокируйте в suspend-функциях: Используйте delay() вместо Thread.sleep().

FAQ: Часто задаваемые вопросы

В чём основное преимущество корутин перед RxJava?

Корутины — более легковесное и нативное решение для Kotlin. Они проще в изучении, требуют меньше бойлерплейта и интегрированы в язык. Flow, часть экосистемы корутин, предоставляет аналогичные RxJava возможности, но с более простым синтаксисом.

Можно ли использовать корутины в Java-проектах?

Технически можно, но это неудобно. Корутины — фича Kotlin, и их использование из Java будет громоздким. Рекомендуется использовать их только в Kotlin-коде.

Сколько корутин можно создать?

Десятки тысяч, а то и миллионы, в зависимости от доступной памяти. Они занимают очень мало места по сравнению с потоками (десятки байт против мегабайт).

Нужно ли закрывать/останавливать корутины?

Да, важно управлять их жизненным циклом. Используйте скоупы с привязкой к жизненному циклу компонентов (например, в Android). При отмене скоупа отменяются все запущенные в нём корутины.

Когда использовать Flow, а когда Channel?

Flow — для холодных потоков данных (данные начинают поступать при появлении подписчика). Channel — для горячих потоков и коммуникации между корутинами (данные производятся независимо от наличия подписчиков).