Корутины Kotlin: От теории к практике с живыми примерами использования

Корутины Kotlin: От теории к практике с живыми примерами использования

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

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

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

Корутины не создают новые потоки! Они работают в существующих потоках, переключая контекст выполнения. Одна корутина может быть приостановлена, а другая — запущена в том же потоке.

Практические примеры использования корутин

1. Сетевые запросы в Android-приложении

Самый распространенный сценарий — выполнение HTTP-запросов без блокировки UI-потока:

// ViewModel или Presenter
suspend fun loadUserData(userId: String): User {
    return withContext(Dispatchers.IO) {
        // Эта часть выполняется в фоновом потоке
        val response = apiService.getUser(userId)
        if (response.isSuccessful) {
            response.body() ?: throw Exception("Данные не получены")
        } else {
            throw Exception("Ошибка сервера")
        }
    }
    // Автоматическое возвращение в Main поток после withContext
}

// Вызов в UI-потоке (Activity/Fragment)
lifecycleScope.launch {
    try {
        val user = loadUserData("123")
        updateUI(user)
    } catch (e: Exception) {
        showError(e.message)
    }
}

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

Когда нужно загрузить данные из нескольких источников одновременно:

suspend fun loadDashboardData(): DashboardData {
    val deferredUser = async { userRepository.getCurrentUser() }
    val deferredNotifications = async { notificationRepository.getUnread() }
    val deferredStats = async { statsRepository.getMonthlyStats() }
    
    // Все три запроса выполняются параллельно!
    return DashboardData(
        user = deferredUser.await(),
        notifications = deferredNotifications.await(),
        stats = deferredStats.await()
    )
}

3. Обработка потоков данных с Flow

Kotlin Flow — это реактивные потоки на основе корутин:

fun getLiveSensorData(): Flow = flow {
    while (true) {
        val data = sensor.read() // Блокирующая операция
        emit(data)
        delay(1000) // Приостанавливаем корутину на 1 секунду
    }
}.flowOn(Dispatchers.IO) // Выполняем в IO-потоке

// Сбор в UI
lifecycleScope.launch {
    getLiveSensorData()
        .filter { it.value > threshold }
        .collect { data ->
            updateSensorDisplay(data)
        }
}

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

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

suspend fun fetchWithTimeout() {
    try {
        val result = withTimeout(5000) { // Таймаут 5 секунд
            longRunningOperation()
        }
        processResult(result)
    } catch (e: TimeoutCancellationException) {
        showMessage("Операция заняла слишком много времени")
    }
}

// Отмена при уходе с экрана
lifecycleScope.launch {
    val job = launch {
        repeat(1000) { i ->
            delay(1000)
            log("Tick $i")
        }
    }
    
    // При уничтожении lifecycleScope все запущенные корутины
    // будут автоматически отменены
}

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

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

Используйте supervisorScope когда нужно, чтобы ошибка в одной корутине не отменяла все остальные. Особенно полезно в UI-приложениях.

Корутины в различных архитектурах

  1. MVVM с ViewModel: Используйте viewModelScope для автоматической отмены при очистке ViewModel
  2. Clean Architecture: Создавайте suspend-функции в UseCase, вызывайте их из ViewModel
  3. Backend на Ktor: Весь фреймворк построен на корутинах, что позволяет обрабатывать тысячи одновременных соединений

FAQ: Часто задаваемые вопросы о корутинах

Чем корутины отличаются от потоков (Threads)?

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

Когда использовать Dispatchers.IO, а когда Dispatchers.Default?

Dispatchers.IO — для блокирующих операций (сеть, файловая система). Dispatchers.Default — для CPU-интенсивных задач (сортировка, вычисления).

Как тестировать код с корутинами?

Используйте runTest из библиотеки kotlinx-coroutines-test. Она позволяет контролировать виртуальное время и избегать реальных задержек.

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

Да, но с ограничениями. Вы можете вызывать suspend-функции из Java через CompletableFuture или callback-функции.

Что такое structured concurrency?

Это принцип, согласно которому все корутины запускаются в определенном scope, и их жизненный цикл управляется этим scope. Это предотвращает утечки и упрощает отмену операций.