Форум программистов, компьютерный форум, киберфорум
mobDevWorks
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Как загружать данные в Kotlin с корутинами (Первая часть)

Запись от mobDevWorks размещена 22.08.2025 в 21:01
Показов 4257 Комментарии 0

Нажмите на изображение для увеличения
Название: Как загружать данные в Kotlin с корутинами.jpg
Просмотров: 326
Размер:	227.4 Кб
ID:	11065
Помню, как пять лет назад я сидел с ноутбуком в любимой кофейне и пытался разобраться с очередным NullPointerException в коде загрузки данных. Тогда я еще использовал AsyncTask и Thread, постоянно боролся с утечками памяти в ViewModel и проклинал callback hell. Знакомая картина?

Переход на корутины Kotlin стал для меня настоящим откровением - не сразу, конечно. Первые месяцы я относился к ним с подозрением, как к очередной "серебряной пуле", которая обещает решить все проблемы, но на деле создает новые. Однако время показало: корутины действительно меняют правила игры. Что меня поразило больше всего? Простота. Там, где раньше требовалось писать десятки строк кода с обработкой потоков, колбэков и состояний, теперь достаточно нескольких элегантных строк. Но дело не только в количестве кода - меняется сам способ мышления.

Раньше загрузка данных напоминала жонглирование. Нужно было одновременно следить за основным потоком, фоновыми операциями, состоянием UI, жизненным циклом компонентов. Малейшая ошибка - и приложение падает или начинает течь память. С корутинами асинхронность стала читаться как обычный синхронный код.

Особенно впечатлила структурированная конкуренция (Structured Concurrency). Наконец появился механизм, который автоматически управляет жизненным циклом задач. Больше не нужно помнить об отмене операций при уничтожении активности - корутины делают это сами.

За эти годы я мигрировал десятки проектов с RxJava, AsyncTask и чистых потоков на корутины. Каждый раз результат превосходил ожидания: код становился читаемее, багов меньше, производительность выше. Конечно, корутины не панацея. У них есть подводные камни, которые я изучил на собственных ошибках. Неправильное использование GlobalScope, проблемы с тестированием, тонкости работы с Flow - обо всем этом расскажу дальше.

Основы работы с корутинами в Kotlin



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

Kotlin
1
2
3
4
5
suspend fun loadUserData(userId: String): User {
    val user = userApi.getUser(userId) // приостанавливается здесь
    val preferences = preferencesApi.getUserPrefs(userId) // и здесь
    return user.copy(preferences = preferences)
}
Этот код выглядит синхронным, но работает асинхронно. Никаких колбэков, никаких onSuccess и onError - просто последовательные вызовы.
Корутины не создаются сами по себе. Им нужен CoroutineScope - контекст выполнения, который определяет их жизненный цикл. В Android часто используется viewModelScope или lifecycleScope:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
class HomeViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.fetchHomeData()
                _uiState.value = HomeUiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = HomeUiState.Error(e.message)
            }
        }
    }
}
Билдер launch запускает корутину типа "огонь и забыли" - она выполняется в фоне и не возвращает результат. Для получения результата используется async:

Kotlin
1
2
val deferred = async { loadUserProfile() }
val profile = deferred.await() // получаем результат
Важная особенность: корутины следуют принципу структурированной конкуренции. Если родительский scope отменяется, все дочерние корутины автоматически останавливаются. Это предотвращает утечки ресурсов.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
class DataRepository {
    private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    
    fun loadMultipleDataSources() = repositoryScope.launch {
        val users = async { loadUsers() }
        val posts = async { loadPosts() }
        val comments = async { loadComments() }
        
        // Все три запроса выполняются параллельно
        val results = awaitAll(users, posts, comments)
    }
}
Другой важный момент - обработка исключений. В корутинах есть два типа билдеров: launch (который не пробрасывает исключения выше) и async (который пробрасывает при вызове await()).

Kotlin
1
2
3
4
5
6
7
8
9
10
// Исключение поглощается
launch {
    throw RuntimeException("Boom!") // не упадёт
}
 
// Исключение всплывает
val result = async {
    throw RuntimeException("Boom!") // упадёт при await()
}
result.await() // здесь получим исключение
Для корректной обработки ошибок используйте try-catch блоки внутри корутин или CoroutineExceptionHandler:

Kotlin
1
2
3
4
5
val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception")
}
 
val scope = CoroutineScope(Job() + handler)
Корутины также поддерживают отмену через Job.cancel(). При отмене корутина получает CancellationException и может выполнить cleanup операции:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
val job = launch {
    try {
        repeat(1000) { i ->
            delay(100)
            println("Working $i")
        }
    } finally {
        println("Cleanup resources")
    }
}
 
delay(500)
job.cancel() // отменяем через полсекунды

Каким самым простым методом (Android приложение на Kotlin ) можно забрать данные с сервера?
Добрый день. Проект состоит из: - Контроллера, который отправляет данные на сервер GET...

Как при появлении Wifi загружать данные
Есть приложение. В нем есть много фрагментов которые подгружают данные из сервера, но юзер зашел в...

Kotlin "it" как перевести на java?
есть пример кода на котлине: override fun one(id: Long): Observable<Banner> { return...

Как исправить утечку памяти Android Studio Kotlin
Я обращаюсь к базе данных получаю ответ в json и процессе того как обрабатывается возникает ошибка....


Suspend-функции и их роль в асинхронности



Если корутины - это сердце асинхронности в Kotlin, то suspend-функции - её артерии. Без них вся система просто не работала бы. Помню, как впервые увидел это ключевое слово и подумал: "Ну вот, ещё одна абстракция". Как же я ошибался.

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

Kotlin
1
2
3
4
5
6
7
suspend fun fetchUserProfile(userId: String): UserProfile {
    val basicInfo = userService.getBasicInfo(userId) // приостановка #1
    val avatar = imageService.loadAvatar(basicInfo.avatarUrl) // приостановка #2
    val settings = settingsService.getUserSettings(userId) // приостановка #3
    
    return UserProfile(basicInfo, avatar, settings)
}
Каждый вызов к внешнему сервису может занимать секунды, но поток не блокируется. Пока мы ждём ответ от userService.getBasicInfo(), другие корутины спокойно выполняют свою работу на том же потоке. Магия suspend-функций кроется в компиляторе. Он превращает такой линейный код в конечный автомат с колбэками под капотом. Но нам, разработчикам, не нужно об этом думать - мы пишем код так, будто он синхронный.
Suspend-функции имеют интересное ограничение: их можно вызывать только из других suspend-функций или из корутин. Попробуйте вызвать delay(1000) из обычной функции - компилятор не даст:

Kotlin
1
2
3
4
5
6
7
fun regularFunction() {
    delay(1000) // ошибка компиляции!
}
 
suspend fun suspendFunction() {
    delay(1000) // всё хорошо
}
Это ограничение существует не просто так. Suspend-функция может быть приостановлена, значит, её нельзя вызывать там, где ожидается немедленный результат. Это защищает от случайного блокирования основного потока.
Часто возникает вопрос: когда делать функцию suspend, а когда нет? Правило простое - если функция выполняет долгую операцию (сеть, база данных, файловая система) или вызывает другие suspend-функции, делайте её suspend.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
// НЕ suspend - быстрая операция в памяти
fun calculateSum(a: Int, b: Int): Int = a + b
 
// suspend - операция с базой данных
suspend fun saveUser(user: User): Long = userDao.insert(user)
 
// suspend - вызывает другие suspend-функции
suspend fun syncUserData(userId: String): SyncResult {
    val remoteUser = fetchRemoteUser(userId)
    val localId = saveUser(remoteUser)
    return SyncResult(localId, remoteUser.lastModified)
}
Интересная деталь: suspend-функции могут быть и не асинхронными. Если внутри нет вызовов других suspend-функций или долгих операций, она выполнится синхронно:

Kotlin
1
2
3
suspend fun fastOperation(): String {
    return "Instant result" // выполняется мгновенно
}
Компилятор достаточно умён, чтобы оптимизировать такие случаи.
Suspend-функции отлично сочетаются с функциями высшего порядка. Можно передавать suspend-лямбды как параметры:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
suspend fun <T> retry(
    times: Int,
    delay: Long = 1000,
    operation: suspend () -> T
): T {
    repeat(times - 1) {
        try {
            return operation()
        } catch (e: Exception) {
            delay(delay)
        }
    }
    return operation() // последняя попытка
}
 
// Использование
val result = retry(3, 2000) {
    apiService.getUserData(userId)
}
Такой подход делает код невероятно композируемым. Можно легко оборачивать любые асинхронные операции в дополнительную логику - retry, кэширование, логирование.
Ещё один мощный паттерн - suspend-свойства. Да, в Kotlin можно делать геттеры suspend:

Kotlin
1
2
3
4
class UserRepository {
    suspend val currentUser: User
        get() = database.getCurrentUser()
}
Это полезно для ленивых вычислений или когда нужно скрыть асинхронную природу получения данных за простым интерфейсом.

Suspend-функции также поддерживают все возможности обычных функций: перегрузку, расширения, инлайн модификатор:

Kotlin
1
2
3
4
5
6
7
8
9
inline suspend fun <T> T.also(crossinline block: suspend (T) -> Unit): T {
    block(this)
    return this
}
 
// Использование
val user = loadUser(id)
    .also { logUserAccess(it) }
    .also { updateLastSeen(it) }
Главное преимущество suspend-функций - они превращают callback hell в линейный, читаемый код. Вместо вложенных колбэков получаем последовательные вызовы:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Было (с колбэками)
userService.getUser(userId) { user ->
    imageService.loadAvatar(user.avatarUrl) { avatar ->
        settingsService.getSettings(userId) { settings ->
            // код обработки вложен на три уровня
            val profile = UserProfile(user, avatar, settings)
            callback.onSuccess(profile)
        }
    }
}
 
// Стало (с suspend-функциями)
suspend fun loadUserProfile(userId: String): UserProfile {
    val user = userService.getUser(userId)
    val avatar = imageService.loadAvatar(user.avatarUrl)
    val settings = settingsService.getSettings(userId)
    return UserProfile(user, avatar, settings)
}
Разница очевидна. Suspend-функции возвращают нам возможность мыслить последовательно, не теряясь в лабиринте вложенных обработчиков.

Контексты выполнения и диспетчеры



Диспетчеры в корутинах - это как выбор правильного инструмента для каждой задачи. Можно забивать гвозди микроскопом, но зачем? За годы работы с корутинами я понял: правильный выбор диспетчера критически важен для производительности приложения.

В Kotlin есть четыре основных диспетчера, каждый оптимизирован под определённый тип операций. Dispatchers.Main работает в главном потоке UI - здесь обновляем интерфейс, меняем состояния экрана. Dispatchers.IO предназначен для операций ввода-вывода: сетевые запросы, чтение файлов, работа с базами данных. Dispatchers.Default используется для CPU-интенсивных вычислений, а Dispatchers.Unconfined - специальный случай для корутин, которые не привязаны к конкретному потоку.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DataProcessor {
    suspend fun processLargeDataset(data: List<String>): ProcessedData = 
        withContext(Dispatchers.Default) {
            // Тяжёлые вычисления на background потоке
            data.map { processItem(it) }
                .groupBy { it.category }
                .mapValues { calculateStats(it.value) }
        }
    
    suspend fun saveToDatabase(result: ProcessedData) = 
        withContext(Dispatchers.IO) {
            // Операции с базой данных
            database.insert(result)
        }
    
    suspend fun updateUI(result: ProcessedData) = 
        withContext(Dispatchers.Main) {
            // Обновление интерфейса
            viewState.value = ViewState.Success(result)
        }
}
withContext - мой любимый способ переключения контекста. Он выполняет блок кода в указанном диспетчере, а затем возвращается в предыдущий контекст. Никаких колбэков, никакой возни с переключением потоков вручную.

Часто вижу ошибку новичков: использование Dispatchers.Main для всего подряд. Результат предсказуем - зависание интерфейса, ANR ошибки, недовольные пользователи. Основной поток должен заниматься только UI, всё остальное - на фоновые потоки. Интересная особенность Dispatchers.IO - он может создавать до 64 потоков по умолчанию, в отличие от Dispatchers.Default, который ограничен количеством процессорных ядер. Это логично: операции ввода-вывода часто блокируются в ожидании, поэтому больше потоков = лучшая утилизация ресурсов.

Kotlin
1
2
3
4
5
6
suspend fun loadMultipleFiles(filePaths: List<String>): List<FileContent> =
    withContext(Dispatchers.IO) {
        filePaths.map { path ->
            async { loadFileContent(path) }
        }.awaitAll()
    }
Создание собственных диспетчеров иногда оправдано. Например, для операций с конкретной базой данных или внешним API:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DatabaseManager {
    private val dbDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    
    suspend fun executeTransaction(block: suspend () -> Unit) = 
        withContext(dbDispatcher) {
            database.beginTransaction()
            try {
                block()
                database.setTransactionSuccessful()
            } finally {
                database.endTransaction()
            }
        }
}
Однопоточный диспетчер гарантирует, что все операции с базой выполняются последовательно, что критично для транзакционной целостности.

Dispatchers.Unconfined - особый зверь. Корутина запускается в текущем потоке, но после первой приостановки может продолжиться в любом потоке. Использую его редко, только для корутин, которые не делают тяжёлой работы и не взаимодействуют с UI:

Kotlin
1
2
3
suspend fun logEvent(event: String) = withContext(Dispatchers.Unconfined) {
    logger.log(event) // быстрая операция
}
Контекст корутины - это больше, чем просто диспетчер. Это комбинация элементов: Job (для отмены), диспетчер (для выполнения), CoroutineName (для отладки), CoroutineExceptionHandler (для обработки ошибок).

Kotlin
1
2
3
4
5
6
7
8
9
10
val customContext = SupervisorJob() + 
                   Dispatchers.IO + 
                   CoroutineName("DataSync") +
                   CoroutineExceptionHandler { _, exception ->
                       handleSyncError(exception)
                   }
 
CoroutineScope(customContext).launch {
    // корутина с настроенным контекстом
}
Переключение контекстов имеет цену - небольшую, но заметную при частом использовании. Поэтому не стоит переключаться без необходимости:

Kotlin
1
2
3
4
5
6
7
8
// Плохо - лишнее переключение
suspend fun loadUser(id: String): User = withContext(Dispatchers.IO) {
    val user = apiService.getUser(id) // уже suspend функция
    return@withContext user
}
 
// Хорошо - используем существующий контекст
suspend fun loadUser(id: String): User = apiService.getUser(id)
Запомните: suspend-функции сами определяют, в каком контексте им лучше выполняться. Ваша задача - не мешать им делать свою работу.

Channel и их применение для межпотоковой коммуникации



Каналы в корутинах - это как конвейерная лента между разными частями приложения. Долгое время я использовал обычные колбэки и shared preferences для передачи данных между компонентами, пока не открыл для себя всю мощь Channel API. Теперь это один из моих любимых инструментов для организации межпотоковой коммуникации.

Channel представляет собой горячий поток данных - producer отправляет значения, consumer их получает. В отличие от обычных suspend-функций, которые возвращают одно значение, каналы могут передавать последовательности данных во времени:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NotificationManager {
    private val notificationChannel = Channel<Notification>(Channel.UNLIMITED)
    
    fun sendNotification(notification: Notification) {
        notificationChannel.trySend(notification)
    }
    
    suspend fun processNotifications() {
        for (notification in notificationChannel) {
            displayNotification(notification)
            delay(500) // пауза между уведомлениями
        }
    }
}
Каналы бывают разных типов, каждый со своим поведением буферизации. Channel.RENDEZVOUS (по умолчанию) не имеет буфера - отправитель блокируется до получения сообщения. Channel.UNLIMITED создаёт неограниченный буфер, Channel.CONFLATED хранит только последнее значение, а числовые значения задают фиксированный размер буфера.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DataStreamer {
    // Буферизованный канал для сглаживания пиков нагрузки
    private val dataChannel = Channel<DataChunk>(capacity = 100)
    
    suspend fun streamData() = coroutineScope {
        // Producer корутина
        launch {
            repeat(1000) { i ->
                val chunk = generateDataChunk(i)
                dataChannel.send(chunk)
            }
            dataChannel.close()
        }
        
        // Consumer корутина
        launch {
            dataChannel.consumeEach { chunk ->
                processChunk(chunk)
            }
        }
    }
}
Интересная особенность каналов - возможность иметь нескольких производителей и потребителей. Это открывает двери для сложных паттернов распределения нагрузки:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WorkDistributor {
    private val taskChannel = Channel<Task>()
    
    fun startWorkers(workerCount: Int) = coroutineScope {
        // Несколько worker корутин читают из одного канала
        repeat(workerCount) { workerId ->
            launch {
                for (task in taskChannel) {
                    println("Worker $workerId processing ${task.id}")
                    processTask(task)
                }
            }
        }
    }
    
    suspend fun submitTask(task: Task) {
        taskChannel.send(task)
    }
}
Channel.CONFLATED особенно полезен для состояний UI, где нужно только последнее значение:

Kotlin
1
2
3
4
5
6
7
8
9
10
class LocationTracker {
    private val locationChannel = Channel<Location>(Channel.CONFLATED)
    
    fun updateLocation(location: Location) {
        locationChannel.trySend(location) // не блокируется
    }
    
    suspend fun observeLocation(): Flow<Location> = 
        locationChannel.receiveAsFlow()
}
Закрытие каналов - важный аспект управления ресурсами. Незакрытый канал может привести к утечкам памяти или зависшим корутинам:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FileProcessor {
    suspend fun processFiles(filePaths: List<String>) = coroutineScope {
        val fileChannel = Channel<File>()
        
        launch {
            try {
                filePaths.forEach { path ->
                    fileChannel.send(File(path))
                }
            } finally {
                fileChannel.close() // важно закрыть канал
            }
        }
        
        launch {
            try {
                for (file in fileChannel) {
                    processFile(file)
                }
            } catch (e: ClosedReceiveChannelException) {
                println("Channel closed, finishing processing")
            }
        }
    }
}
Каналы отлично подходят для реализации паттерна "fan-out" - когда один производитель отправляет данные нескольким потребителям, или "fan-in" - когда несколько производителей отправляют данные одному потребителю:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LogAggregator {
    private val logChannel = Channel<LogEntry>()
    
    // Fan-in: несколько источников логов
    fun collectLogs() = coroutineScope {
        launch { collectWebServerLogs() }
        launch { collectDatabaseLogs() }
        launch { collectApplicationLogs() }
        
        // Один потребитель обрабатывает все логи
        launch {
            for (logEntry in logChannel) {
                saveLogToFile(logEntry)
            }
        }
    }
    
    private suspend fun collectWebServerLogs() {
        webServer.logStream().collect { log ->
            logChannel.send(LogEntry.Web(log))
        }
    }
}
Обработка ошибок в каналах требует внимания. Исключение в producer может привести к неожиданному закрытию канала:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
suspend fun robustChannelProcessing() = coroutineScope {
    val channel = Channel<String>()
    
    launch {
        try {
            repeat(10) { i ->
                if (i == 5) throw RuntimeException("Oops")
                channel.send("Message $i")
            }
        } catch (e: Exception) {
            channel.close(e) // закрываем с ошибкой
        }
    }
    
    launch {
        try {
            for (message in channel) {
                println(message)
            }
        } catch (e: ClosedReceiveChannelException) {
            println("Channel closed: ${e.cause}")
        }
    }
}
Каналы можно комбинировать с Flow для создания мощных data processing pipeline. Это особенно удобно для обработки потоков данных в реальном времени, когда нужна как буферизация, так и трансформация данных.

Scope и Job: иерархия выполнения задач



Job - это сердце каждой корутины, её паспорт и контроллер жизненного цикла одновременно. Каждая корутина получает Job автоматически, но понимание того, как они работают и взаимодействуют друг с другом, кардинально меняет подход к написанию асинхронного кода. Когда создается корутина, она автоматически становится дочерней по отношению к Job своего scope. Это создает иерархическую структуру, где родительские Job контролируют дочерние:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DataSyncManager {
    private val syncScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    
    fun startComplexSync() {
        val parentJob = syncScope.launch {
            println("Parent job started")
            
            val childJob1 = launch {
                delay(2000)
                println("Child 1 completed")
            }
            
            val childJob2 = launch {
                delay(3000) 
                println("Child 2 completed")
            }
            
            // Родитель ждет завершения всех детей
            childJob1.join()
            childJob2.join()
            println("Parent job completed")
        }
    }
}
Самая важная особенность такой иерархии - структурированная конкуренция. Если родительский Job отменяется, все дочерние Job тоже отменяются автоматически. Это предотвращает утечки ресурсов и зависшие задачи.
Существует два основных типа Job: обычный Job() и SupervisorJob(). Различие критично для обработки ошибок. При обычном Job исключение в любой дочерней корутине приводит к отмене всех остальных дочерних корутин:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
val regularScope = CoroutineScope(Job())
 
regularScope.launch {
    launch { 
        delay(1000)
        throw RuntimeException("Child failed!")
    }
    
    launch {
        delay(2000) 
        println("This will never print") // отменится из-за исключения в соседней корутине
    }
}
SupervisorJob ведет себя иначе - исключение в одной дочерней корутине не влияет на другие:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
val supervisorScope = CoroutineScope(SupervisorJob())
 
supervisorScope.launch {
    launch { 
        delay(1000)
        throw RuntimeException("Child failed!")
    }
    
    launch {
        delay(2000)
        println("This will print") // продолжит выполняться
    }
}
Отмена Job - одна из самых мощных возможностей корутин. Она происходит кооперативно - корутина должна проверять состояние отмены и реагировать на CancellationException:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LongRunningTask {
    suspend fun processLargeDataset(data: List<String>): List<ProcessedItem> {
        val results = mutableListOf<ProcessedItem>()
        
        for (item in data) {
            ensureActive() // проверяем, не отменена ли корутина
            
            val processed = heavyProcessing(item)
            results.add(processed)
            
            if (results.size % 100 == 0) {
                yield() // даем другим корутинам возможность выполниться
            }
        }
        
        return results
    }
}
Job имеют состояния жизненного цикла: New, Active, Completing, Completed, Cancelling, Cancelled. Можно отслеживать эти состояния для реализации сложной логики:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val job = launch {
    try {
        longRunningOperation()
    } finally {
        if (!isActive) {
            println("Operation was cancelled, cleaning up...")
            cleanup()
        }
    }
}
 
// В другом месте
if (job.isActive) {
    job.cancel("User requested cancellation")
}
CoroutineScope инкапсулирует Job и предоставляет контекст для запуска корутин. В Android часто используются готовые scope: viewModelScope, lifecycleScope, но можно создавать собственные:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
class NetworkRepository {
    private val repositoryJob = SupervisorJob()
    private val repositoryScope = CoroutineScope(repositoryJob + Dispatchers.IO)
    
    fun fetchData() = repositoryScope.launch {
        // операции с сетью
    }
    
    fun cleanup() {
        repositoryJob.cancelChildren() // отменяем все дочерние задачи
    }
}
Полезный паттерн - использование coroutineScope для создания временного scope внутри suspend-функции. Такой scope автоматически дождется завершения всех дочерних корутин:

Kotlin
1
2
3
4
5
6
7
suspend fun parallelDataProcessing(items: List<Item>) = coroutineScope {
    val chunks = items.chunked(100)
    
    chunks.map { chunk ->
        async { processChunk(chunk) }
    }.awaitAll().flatten()
}
Job можно комбинировать для создания сложных сценариев управления жизненным циклом. Например, задача, которая должна завершиться при отмене любого из нескольких условий:

Kotlin
1
2
3
4
5
6
val userSessionJob = SupervisorJob()
val networkConnectivityJob = SupervisorJob()
 
val dataFetchJob = CoroutineScope(userSessionJob + networkConnectivityJob).launch {
    // задача отменится при потере сессии ИЛИ сетевого соединения
}
Понимание иерархии Job и правильное использование scope кардинально улучшает архитектуру приложения, делая его более предсказуемым и устойчивым к ошибкам.

Structured Concurrency и управление жизненным циклом корутин



Structured Concurrency - это не просто красивый термин из документации, а фундаментальный принцип, который спасает от бесконечных головных болей с асинхронным кодом. До его появления я постоянно сталкивался с ситуациями, когда фоновые задачи продолжали работать после закрытия экрана, потребляя ресурсы и иногда даже падая с исключениями.

Суть концепции проста: корутины организованы в древовидную структуру, где каждая дочерняя корутина привязана к родительскому scope. Когда родитель завершается или отменяется, все дочерние корутины автоматически останавливаются. Никаких забытых задач, никаких утечек памяти.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UserProfileManager {
    private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
    
    fun loadUserData(userId: String) {
        managerScope.launch { // родительская корутина
            val profile = async { userApi.getProfile(userId) } // дочерняя #1
            val friends = async { userApi.getFriends(userId) } // дочерняя #2  
            val posts = async { userApi.getPosts(userId) }     // дочерняя #3
            
            // Все три запроса выполняются параллельно
            val userData = UserData(
                profile = profile.await(),
                friends = friends.await(), 
                posts = posts.await()
            )
            
            updateUI(userData)
        }
    }
    
    fun cleanup() {
        managerScope.cancel() // отменяет ВСЕ активные корутины
    }
}
Самый яркий пример structured concurrency в действии - Android ViewModel. Когда экран закрывается, viewModelScope автоматически отменяется, унося с собой все запущенные корутины. Раньше приходилось помнить про отмену каждой AsyncTask вручную:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class HomeViewModel : ViewModel() {
    private val _homeState = MutableStateFlow<HomeState>(HomeState.Loading)
    val homeState = _homeState.asStateFlow()
    
    fun loadHomeData() {
        viewModelScope.launch {
            try {
                // Даже если пользователь быстро покинет экран,
                // эта корутина автоматически отменится
                val news = async { newsRepository.getLatestNews() }
                val weather = async { weatherRepository.getCurrentWeather() }
                val recommendations = async { recommendationRepository.getPersonalized() }
                
                _homeState.value = HomeState.Loaded(
                    news = news.await(),
                    weather = weather.await(), 
                    recommendations = recommendations.await()
                )
            } catch (e: CancellationException) {
                // Корутина была отменена - это нормально
                throw e
            } catch (e: Exception) {
                _homeState.value = HomeState.Error(e.message)
            }
        }
    }
}
Важная деталь: CancellationException не нужно обрабатывать в блоке catch - её следует пробрасывать выше. Это сигнал системе о корректной отмене корутины.

Structured concurrency работает и для вложенных suspend-функций через coroutineScope. Такая функция создаёт новый scope, который автоматически дожидается завершения всех дочерних корутин:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
suspend fun synchronizeUserData(userId: String): SyncResult = coroutineScope {
    val uploadJob = async { uploadPendingChanges(userId) }
    val downloadJob = async { downloadRemoteChanges(userId) }
    val cleanupJob = async { cleanupObsoleteData() }
    
    // Функция не завершится, пока все три операции не закончатся
    SyncResult(
        uploaded = uploadJob.await(),
        downloaded = downloadJob.await(),
        cleaned = cleanupJob.await()
    )
}
Если любая из дочерних корутин упадёт с исключением, остальные будут отменены, а исключение всплывёт наверх. Это предотвращает частичное выполнение операций и поддерживает консистентность данных.

Структурированная конкуренция особенно важна при работе с ресурсами. Представьте загрузку файла: даже если пользователь отменил операцию, корутина должна корректно закрыть потоки и удалить временные файлы:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
suspend fun downloadFile(url: String, destination: File) = coroutineScope {
    val connection = openConnection(url)
    
    try {
        val downloadJob = async {
            connection.inputStream.use { input ->
                destination.outputStream().use { output ->
                    input.copyTo(output)
                }
            }
        }
        
        downloadJob.await()
    } finally {
        connection.disconnect() // выполнится даже при отмене
    }
}
Блок finally гарантированно выполнится при любом сценарии завершения корутины - успешном завершении, исключении или отмене. Structured concurrency делает код не только безопаснее, но и понятнее. Глядя на иерархию корутин, легко понять жизненный цикл каждой операции и её зависимости от других задач.

Практические паттерны загрузки данных



После всех теоретических основ пора переходить к реальной работе. За пять лет использования корутин я выработал несколько базовых паттернов, которые покрывают 90% задач по загрузке данных. Эти подходы проверены в боевых условиях - от стартапов до энтерпрайз-проектов с миллионами пользователей. Первый и самый важный принцип: разделяйте источники данных и их представление. Repository должен предоставлять данные в удобном для потребления виде, скрывая детали получения - сеть, база, кэш или что-то ещё. Корутины идеально подходят для этой задачи:

Kotlin
1
2
3
4
5
interface ArticleRepository {
    suspend fun getArticles(): List<Article>
    suspend fun getArticle(id: String): Article
    fun observeArticles(): Flow<List<Article>>
}
Suspend-функции для разовых операций, Flow для реактивных потоков данных. Просто и эффективно.
Второй паттерн - всегда думайте об ошибках заранее. Сеть может упасть, база данных заблокироваться, а пользователь в любой момент может закрыть приложение. Корутины предоставляют элегантные способы обработки всех этих сценариев:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
 
suspend fun <T> safeApiCall(apiCall: suspend () -> T): Result<T> {
    return try {
        Result.Success(apiCall())
    } catch (e: Exception) {
        Result.Error(e)
    }
}
Третий принцип касается композиции операций. Очень часто нужно загрузить данные из нескольких источников, объединить их или применить трансформации. Корутины позволяют делать это декларативно:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
suspend fun loadUserDashboard(userId: String): DashboardData = coroutineScope {
    val profile = async { userService.getProfile(userId) }
    val notifications = async { notificationService.getUnread(userId) }
    val activities = async { activityService.getRecent(userId) }
    
    DashboardData(
        user = profile.await(),
        unreadCount = notifications.await().size,
        recentActivities = activities.await().take(5)
    )
}
Параллельное выполнение получается естественно, без дополнительных усилий.
Четвёртый важный аспект - управление состоянием загрузки. UI должен показывать индикаторы загрузки, обрабатывать ошибки, предоставлять возможность повторить операцию. StateFlow отлично подходит для этих задач, создавая реактивную связь между данными и интерфейсом.

Repository Pattern с корутинами



Repository Pattern с корутинами превращается в настоящую находку для архитектуры приложения. Этот паттерн я использую практически во всех проектах - он создаёт чистую абстракцию над источниками данных и прекрасно сочетается с асинхронной природой корутин. Классический подход к реализации repository выглядит примерно так: интерфейс определяет контракт, а конкретная реализация скрывает детали работы с API, базой данных или кэшем. С корутинами этот паттерн становится ещё мощнее:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface UserRepository {
    suspend fun getUser(id: String): User
    suspend fun updateUser(user: User): User
    suspend fun deleteUser(id: String): Boolean
    fun observeUser(id: String): Flow<User>
}
 
class UserRepositoryImpl(
    private val userApi: UserApi,
    private val userDao: UserDao,
    private val cacheManager: CacheManager
) : UserRepository {
    
    override suspend fun getUser(id: String): User {
        return try {
            // Сначала пробуем кэш
            cacheManager.getUser(id) ?: run {
                // Затем API
                val user = userApi.getUser(id)
                cacheManager.saveUser(user)
                userDao.insertUser(user)
                user
            }
        } catch (networkError: NetworkException) {
            // Fallback на локальную базу
            userDao.getUser(id) ?: throw UserNotFoundException(id)
        }
    }
}
Такая реализация автоматически обрабатывает несколько источников данных, кэширование и fallback сценарии. Все асинхронные операции скрыты за простым suspend интерфейсом.
Особенно элегантно Repository Pattern работает с Flow для реактивных данных. Можно легко создать поток, который объединяет локальные изменения с обновлениями с сервера:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ArticleRepositoryImpl : ArticleRepository {
    private val localArticles = MutableSharedFlow<List<Article>>()
    
    override fun observeArticles(): Flow<List<Article>> = flow {
        // Сначала отдаём кэшированные данные
        val cached = articleDao.getAllArticles()
        emit(cached)
        
        // Затем обновляем с сервера
        try {
            val fresh = articleApi.getArticles()
            articleDao.insertAll(fresh)
            emit(fresh)
        } catch (e: Exception) {
            // Если сервер недоступен, продолжаем с кэшированными данными
        }
    }.distinctUntilChanged()
    
    override suspend fun refreshArticles() {
        val fresh = articleApi.getArticles()
        articleDao.insertAll(fresh)
        localArticles.emit(articleDao.getAllArticles())
    }
}
Repository также отлично подходит для реализации сложных бизнес-операций, которые затрагивают несколько источников данных:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class OrderRepository(
    private val orderApi: OrderApi,
    private val paymentApi: PaymentApi,
    private val inventoryApi: InventoryApi
) {
    
    suspend fun placeOrder(order: Order): OrderResult = coroutineScope {
        val inventoryCheck = async { inventoryApi.checkAvailability(order.items) }
        val paymentAuth = async { paymentApi.authorizePayment(order.payment) }
        
        val availabilityResult = inventoryCheck.await()
        val paymentResult = paymentAuth.await()
        
        if (availabilityResult.isSuccess && paymentResult.isSuccess) {
            val finalOrder = orderApi.createOrder(order)
            inventoryApi.reserveItems(order.items)
            OrderResult.Success(finalOrder)
        } else {
            OrderResult.Failed("Inventory or payment failed")
        }
    }
}
Параллельная проверка наличия товаров и авторизация платежа происходят одновременно, что сокращает общее время операции.
Один из полезных паттернов - кэширование с TTL (Time To Live) внутри repository:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WeatherRepository {
    private var cachedWeather: Weather? = null
    private var cacheTimestamp: Long = 0
    private val cacheTimeout = TimeUnit.MINUTES.toMillis(10)
    
    override suspend fun getCurrentWeather(): Weather {
        val now = System.currentTimeMillis()
        
        return if (cachedWeather != null && (now - cacheTimestamp) < cacheTimeout) {
            cachedWeather!!
        } else {
            val fresh = weatherApi.getCurrentWeather()
            cachedWeather = fresh
            cacheTimestamp = now
            fresh
        }
    }
}
Repository Pattern с корутинами создаёт чистую архитектуру, где бизнес-логика не зависит от деталей получения данных, а асинхронные операции выглядят как обычный последовательный код.

Обработка исключений в suspend-функциях



Обработка ошибок в корутинах - это та область, где я наделал больше всего ошибок в начале своего пути. Казалось бы, что сложного? Обычный try-catch, как в синхронном коде. Но корутины добавляют свои нюансы, которые могут превратить отладку в настоящий кошмар, если не понимать механизмы работы исключений. Главное отличие suspend-функций от обычных - способ распространения исключений. В обычном коде исключение просто всплывает по стеку вызовов. В корутинах всё сложнее: исключения могут пробрасываться между потоками, теряться в Job иерархии или неожиданно отменять соседние корутины.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DataService {
    suspend fun loadCriticalData(): CriticalData {
        return try {
            val primaryData = primaryApi.getData()
            val secondaryData = secondaryApi.getData()
            CriticalData(primaryData, secondaryData)
        } catch (e: NetworkException) {
            // Специфичная обработка сетевых ошибок
            val fallbackData = localCache.getCachedData()
            if (fallbackData != null) {
                CriticalData.fromCache(fallbackData)
            } else {
                throw ServiceUnavailableException("No data available", e)
            }
        } catch (e: CancellationException) {
            // ВАЖНО: CancellationException нужно пробрасывать дальше
            throw e
        }
    }
}
CancellationException требует особого обращения. Это исключение сигнализирует о корректной отмене корутины, и его нельзя "поглощать" в catch блоках. Если не пробросить CancellationException выше, корутина не отменится должным образом, что может привести к утечкам ресурсов.
Структурированная обработка ошибок работает по-разному в зависимости от типа Job. С обычным Job любое необработанное исключение в дочерней корутине приводит к отмене всех остальных дочерних корутин:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val parentScope = CoroutineScope(Job() + Dispatchers.IO)
 
parentScope.launch {
    val job1 = launch {
        delay(1000)
        throw RuntimeException("Job1 failed")
    }
    
    val job2 = launch {
        delay(2000)
        println("Job2 completed") // никогда не выполнится
    }
    
    // Обе корутины отменятся из-за исключения в job1
}
SupervisorJob изолирует ошибки между дочерними корутинами:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
 
supervisorScope.launch {
    val job1 = launch {
        delay(1000)
        throw RuntimeException("Job1 failed")
    }
    
    val job2 = launch {
        delay(2000)
        println("Job2 completed") // выполнится успешно
    }
    
    // job2 продолжит работать несмотря на ошибку в job1
}
Для централизованной обработки ошибок используется CoroutineExceptionHandler. Он срабатывает только для необработанных исключений в корутинах, запущенных через launch:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    when (exception) {
        is NetworkException -> logNetworkError(exception)
        is DatabaseException -> reportDatabaseIssue(exception)
        else -> logUnknownError(exception)
    }
}
 
val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
 
scope.launch {
    throw NetworkException("Connection failed") // будет обработано handler'ом
}
Важная деталь: CoroutineExceptionHandler НЕ работает с async корутинами. Исключения в async всплывают при вызове await():

Kotlin
1
2
3
4
5
6
7
8
9
val deferred = async {
    throw RuntimeException("Async failed")
}
 
try {
    val result = deferred.await() // исключение всплывёт здесь
} catch (e: Exception) {
    handleError(e)
}
Для создания отказоустойчивых операций я часто использую паттерн Result wrapper:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val exception: Throwable) : ApiResult<Nothing>()
}
 
suspend fun <T> safeApiCall(call: suspend () -> T): ApiResult<T> {
    return try {
        ApiResult.Success(call())
    } catch (e: CancellationException) {
        throw e // пробрасываем CancellationException
    } catch (e: Exception) {
        ApiResult.Error(e)
    }
}
 
// Использование
suspend fun loadUserProfile(id: String): ApiResult<UserProfile> {
    return safeApiCall { userApi.getProfile(id) }
}
Комбинирование нескольких потенциально ошибочных операций требует особого внимания к обработке частичных сбоев:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
suspend fun loadDashboardData(): DashboardResult = coroutineScope {
    val newsDeferred = async { safeApiCall { newsApi.getLatestNews() } }
    val weatherDeferred = async { safeApiCall { weatherApi.getCurrentWeather() } }
    val stocksDeferred = async { safeApiCall { stocksApi.getPortfolio() } }
    
    val newsResult = newsDeferred.await()
    val weatherResult = weatherDeferred.await() 
    val stocksResult = stocksDeferred.await()
    
    DashboardResult(
        news = if (newsResult is ApiResult.Success) newsResult.data else emptyList(),
        weather = if (weatherResult is ApiResult.Success) weatherResult.data else null,
        stocks = if (stocksResult is ApiResult.Success) stocksResult.data else emptyList(),
        hasErrors = listOf(newsResult, weatherResult, stocksResult).any { it is ApiResult.Error }
    )
}
Правильная обработка исключений в suspend-функциях делает приложение устойчивым к сбоям и обеспечивает предсказуемое поведение даже в условиях нестабильной сети или других внешних проблем.

Flow как альтернатива колбэкам



Flow - это то, что окончательно убедило меня в превосходстве корутин над RxJava. Помню, как изучал reactive streams в Java, пытался понять операторы типа flatMapLatest и switchMap, а потом увидел Flow API. Всё стало на свои места - та же мощь реактивного программирования, но с читаемым синтаксисом и без безумной кривой обучения.

По сути Flow - это последовательность значений, которые вычисляются асинхронно. В отличие от suspend-функций, которые возвращают одно значение, Flow может эмитировать множество значений во времени. Это идеально подходит для потоков данных: обновления UI, события пользователя, изменения в базе данных.

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LocationService {
    fun observeLocation(): Flow<Location> = flow {
        while (true) {
            val currentLocation = getCurrentGpsLocation()
            emit(currentLocation)
            delay(5000) // обновляем каждые 5 секунд
        }
    }
    
    suspend fun startLocationTracking() {
        observeLocation().collect { location ->
            updateUI(location)
            saveLocationToDatabase(location)
        }
    }
}
Холодная природа Flow означает, что код внутри блока flow { } не выполняется до вызова терминального оператора типа collect. Это даёт полный контроль над временем выполнения и потреблением ресурсов.

Операторы трансформации Flow работают интуитивно понятно. map преобразует каждое значение, filter отбирает подходящие, take ограничивает количество элементов:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
class NewsRepository {
    fun getBreakingNews(): Flow<List<NewsArticle>> = flow {
        while (true) {
            val allNews = newsApi.getLatestNews()
            emit(allNews)
            delay(TimeUnit.MINUTES.toMillis(1))
        }
    }
    .map { articles -> articles.filter { it.isBreaking } }
    .filter { breakingNews -> breakingNews.isNotEmpty() }
    .distinctUntilChanged()
}
Обработка ошибок в Flow происходит через оператор catch. Он перехватывает исключения в upstream операциях и позволяет восстановиться или эмитировать fallback значения:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
fun observeWeatherData(): Flow<WeatherData> = flow {
    while (true) {
        val weather = weatherApi.getCurrentWeather()
        emit(weather)
        delay(TimeUnit.MINUTES.toMillis(10))
    }
}
.catch { exception ->
    when (exception) {
        is NetworkException -> emit(getCachedWeather())
        else -> emit(WeatherData.unavailable())
    }
}
Flow отлично интегрируется с другими корутинами через операторы типа flatMapConcat и flatMapMerge. Первый выполняет операции последовательно, второй - параллельно:

Kotlin
1
2
3
4
5
6
7
8
9
10
class ImageProcessor {
    fun processImages(imageUrls: Flow<String>): Flow<ProcessedImage> = 
        imageUrls.flatMapMerge(concurrency = 3) { url ->
            flow {
                val downloaded = downloadImage(url)
                val processed = applyFilters(downloaded)
                emit(processed)
            }
        }
}
Комбинирование нескольких Flow выполняется через операторы combine и zip. combine эмитирует новое значение при изменении любого из источников, zip ждёт новых значений от всех источников:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
class DashboardViewModel {
    private val userProfile = userRepository.observeCurrentUser()
    private val unreadMessages = messageRepository.observeUnreadCount() 
    private val notifications = notificationRepository.observeNotifications()
    
    val dashboardState = combine(userProfile, unreadMessages, notifications) { user, messages, notifs ->
        DashboardState(
            user = user,
            unreadCount = messages,
            hasNotifications = notifs.isNotEmpty()
        )
    }
}
Для горячих потоков данных, которые должны существовать независимо от подписчиков, используются SharedFlow и StateFlow. StateFlow хранит текущее состояние и сразу отдаёт его новым подписчикам:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserSessionManager {
    private val _sessionState = MutableStateFlow<SessionState>(SessionState.LoggedOut)
    val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()
    
    fun login(credentials: Credentials) {
        viewModelScope.launch {
            _sessionState.value = SessionState.LoggingIn
            try {
                val session = authService.login(credentials)
                _sessionState.value = SessionState.LoggedIn(session)
            } catch (e: Exception) {
                _sessionState.value = SessionState.LoginFailed(e.message)
            }
        }
    }
}
Flow решает проблему callback hell элегантно и декларативно. Вместо вложенных обработчиков получается читаемая цепочка трансформаций, где каждый этап понятен и тестируем отдельно.

StateFlow и SharedFlow для реактивного программирования



StateFlow и SharedFlow - это эволюция Flow, которая решает конкретную проблему: как сделать холодные потоки горячими. Если обычный Flow начинает работу только при подключении подписчика, то StateFlow и SharedFlow живут своей жизнью независимо от того, кто их слушает. За три года работы с этими инструментами я понял - они кардинально меняют подход к управлению состоянием приложения.

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

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CartManager {
    private val _cartItems = MutableStateFlow<List<CartItem>>(emptyList())
    val cartItems: StateFlow<List<CartItem>> = _cartItems.asStateFlow()
    
    private val _totalPrice = MutableStateFlow(0.0)
    val totalPrice: StateFlow<Double> = _totalPrice.asStateFlow()
    
    fun addItem(item: CartItem) {
        val currentItems = _cartItems.value.toMutableList()
        currentItems.add(item)
        _cartItems.value = currentItems
        updateTotalPrice()
    }
    
    private fun updateTotalPrice() {
        val total = _cartItems.value.sumOf { it.price * it.quantity }
        _totalPrice.value = total
    }
}
StateFlow автоматически применяет distinctUntilChanged - если новое значение равно предыдущему, подписчики не получат уведомление. Это предотвращает лишние перерисовки UI и вычисления.

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

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NotificationManager {
    private val _notifications = MutableSharedFlow<Notification>()
    val notifications: SharedFlow<Notification> = _notifications.asSharedFlow()
    
    private val _toastMessages = MutableSharedFlow<String>(
        replay = 1, // последнее сообщение доступно новым подписчикам
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val toastMessages: SharedFlow<String> = _toastMessages.asSharedFlow()
    
    fun showNotification(notification: Notification) {
        _notifications.tryEmit(notification)
    }
    
    fun showToast(message: String) {
        _toastMessages.tryEmit(message)
    }
}
Параметры SharedFlow позволяют настроить поведение буферизации. replay определяет количество последних событий, доступных новым подписчикам. extraBufferCapacity добавляет дополнительный буфер для случаев медленных подписчиков.

Комбинирование StateFlow создаёт мощные реактивные конструкции. Производное состояние автоматически пересчитывается при изменении источников:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class UserDashboardViewModel {
    private val userProfile = userRepository.observeCurrentUser().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = null
    )
    
    private val notifications = notificationRepository.observeNotifications().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000), 
        initialValue = emptyList()
    )
    
    val dashboardUiState = combine(userProfile, notifications) { user, notifs ->
        when {
            user == null -> DashboardUiState.Loading
            user.isActive -> DashboardUiState.Active(
                user = user,
                notificationCount = notifs.size,
                hasUrgentNotifications = notifs.any { it.isUrgent }
            )
            else -> DashboardUiState.Suspended(user)
        }
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = DashboardUiState.Loading
    )
}
SharingStarted.WhileSubscribed() с таймаутом - оптимальная стратегия для большинства случаев. Поток активен только при наличии подписчиков, но продолжает работать ещё 5 секунд после отписки последнего наблюдателя. Это позволяет избежать повторной инициализации при быстрых переподключениях, например, при поворотах экрана.

StateFlow и SharedFlow отлично подходят для реализации паттерна "единый источник истины". Все изменения состояния проходят через один контролируемый канал, что упрощает отладку и тестирование:

Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class GameStateManager {
    private val _gameState = MutableStateFlow<GameState>(GameState.Menu)
    val gameState: StateFlow<GameState> = _gameState.asStateFlow()
    
    private val _gameEvents = MutableSharedFlow<GameEvent>()
    val gameEvents: SharedFlow<GameEvent> = _gameEvents.asSharedFlow()
    
    fun startGame() {
        _gameState.value = GameState.Playing(level = 1, score = 0)
        _gameEvents.tryEmit(GameEvent.GameStarted)
    }
    
    fun updateScore(newScore: Int) {
        val currentState = _gameState.value
        if (currentState is GameState.Playing) {
            _gameState.value = currentState.copy(score = newScore)
            if (newScore > currentState.score + 100) {
                _gameEvents.tryEmit(GameEvent.ScoreBonus(newScore - currentState.score))
            }
        }
    }
}
Важное различие между emit() и tryEmit(): первый suspend метод, который может приостановиться при переполнении буфера, второй немедленно возвращает результат операции. Для StateFlow обычно используется прямое присваивание .value, для SharedFlow - tryEmit() в неблокирующих операциях.

Как перетащить мусор в корзину? Drag 'n drop в Kotlin
Здравствуйте. Есть мини-игра, где надо перетащить клочок бумаги в корзину. Есть код, который вроде...

[Kotlin] Как узнать и сравнить положение двух imageView?
Есть две картинки: клочок бумаги и корзина. Как сделать так, чтобы бумага исчезала, когда ее...

Как учить Kotlin и стоит ли?
Добрый день, вообще я фрилансер пишу сайты на таких технологиях HTML, CSS, jQuery, WordPress,...

Как последовательно выполнять методы используя kotlin coroutines?
У меня есть картинки в recyclerview по нажатию на которые выполняется запрос к апи, при чём они...

firebase database как получать сообщения и выводить их на экран на kotlin
Не понимаю что нужно писать в onClickListener, чтобы при нажатии на кнопку брался текст с базы...

Как запускать параллельные таймеры (kotlin)?
Программа для раздельного старта спортсменов. Есть EditText для количества участников, по нему...

[Kotlin] Как заставить Service работать, даже если приложение выгружено из списка задач?
Здравствуйте. Пишу приложение для напоминаний. Пользователь вводит текст, который хочет вывести,...

Как встроить admob в kotlin проект?
Есть проект созданный в android studio на kotlin, при встраивание admob проект собирается но при...

Как использовать RxJava2, Retrofit2 в Android Kotlin
Здравствуйте. Пишу приложение, где нужно прочитать JSON файл из интернета и вывести на экран....

Как исправить ошибку Can't create handler inside thread that has not called Looper.prepare() на Kotlin?
У меня есть activity в котором я подключаюсь к веб-сокету. Он должен присылать сообщения с помощью...

Kotlin приложение для просмотра картинок. Как зациклить этот адаптер?
Здравствуйте. Не хватает знаний/мозгов как в идеале сделать &quot;карусель&quot; из картинок (т.е. чтобы...

Как преобразовать Нейросеть в Kotlin из Python?
Код на Python import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) # создаем...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
20. Мат мед. Абсентеизм как отдельный тип простоя
anaschu 29.05.2026
Апдейт модели: исправленные баги, абсентеизм и новые механизмы Продолжаю развивать ранее описанную модель рабочего коллектива на AnyLogic. За последние несколько дней был проведён серьёзный. . .
19. здоровье, усталость и психотип работника влияют на производительность предприятия, и наоборот, производительность на здоровье, усталось и психотип
anaschu 28.05.2026
Дискретно-событийная модель рабочего коллектива на AnyLogic: здоровье, выгорание, психотипы и микростимуляция Привет, коллеги. Хочу поделиться итогами нескольких недель работы над симуляционной. . .
"Прокси" для последовательного порта
Eddy_Em 28.05.2026
Эту штуку написал я достаточно давно. Но сейчас вот понадобилось настроить датчик грозы, но при этом не отключать его от "метеодемона". Соответственно, надо запустить этот "прокси": метеодемон будет. . .
Рефакторинг программы уравнивания.
Massaraksh7 26.05.2026
Пример по предыдущей записи в блоге. Но, надо заметить, что, во-первых, там оптимизация не только математики, но и работы с базой данных, и с графами, а во-вторых, это ещё не всё.
Использование TThread в Lazarus для математических вычислений.
Massaraksh7 25.05.2026
Производя рефакторинг своих программ на предмет ускорения их работы, обратил внимание на такой аспект, как сокращение времени матвычислений. Дело в том, что приходится работать с большими матрицами. . .
Модель здравосохранения 18. Чем здоровее работник, тем быстрее выгорает
anaschu 24.05.2026
Имитационная модель корпоративного здравоохранения: что показывает математика Сегодня в модели рабочего коллектива на AnyLogic появились три новые механики — выгорание через накопленную усталость,. . .
Модель здравосохранения 17. Планы на выгорание
anaschu 23.05.2026
Вот конкретная схема реализации: В классе Работник добавить: накопленнаяУсталость — растёт каждый час работы, снижается в перерывы и болезни коэффициентПрезентеизма — снижает продуктивность. . .
Изменение цветов в палитре gif файла aka фавикона
russiannick 23.05.2026
Изменение цветов в палитре gif файла, юзаемого как фавиконка в составе html-файла, помещенная в base64, средствами нативного Java Script, навеянное сном в майский день. Для работы необходим браузер,. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru