Если вы пишете на 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 — для горячих потоков и коммуникации между корутинами (данные производятся независимо от наличия подписчиков).