Если вы пишете на 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-приложениях.
Корутины в различных архитектурах
- MVVM с ViewModel: Используйте viewModelScope для автоматической отмены при очистке ViewModel
- Clean Architecture: Создавайте suspend-функции в UseCase, вызывайте их из ViewModel
- 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. Это предотвращает утечки и упрощает отмену операций.