Тема 1. Как выглядит Kotlin Coroutine без макияжа
Пытаюсь лучше понять работу Kotlin Coroutine. Беру небольшую тему про Kotlin Coroutine и пытаюсь разобраться и написать шпаргалку. Сегодня про suspend функции.
1. Что такое suspend функция
Suspend-функции — это специальные функции в Kotlin, предназначенные для работы с корутинами. Они позволяют приостанавливать выполнение кода без блокировки потока, что делает их идеальными для асинхронных операций (например, сетевых запросов, работы с базой данных или долгих вычислений). Ключевое слово suspend указывает компилятору, что функция может быть «приостановлена» и позже возобновлена, не занимая поток исполнения.
Мы можем вызвать suspend функцию только из корутины или из другой suspend функции и это гарантирует нам, что мы будем иметь доступ к Continuation и соответственно сможем написать правильную suspend функцию, которая сможет передать результат своей работы через объект Continuation.
Пока звучит путанно, но на примерах всё встанет на свои места.
Пример объявления:
suspend fun fetchData(): String { delay(1000) // Приостановка на 1 секунду return "Данные получены" }
2. Зачем нужны suspend-функции?
Основные цели:
Упрощение асинхронного кода. Вместо использования колбэков или Future разработчики пишут последовательный код, который выглядит как синхронный, но работает асинхронно.
Избежание «callback hell». Suspend-функции позволяют избежать вложенных колбэков, улучшая читаемость.
Эффективное использование ресурсов. Приостановка корутины не блокирует поток, что особенно важно в однопоточных средах (например, Android UI-поток).
Интеграция с корутинами. Suspend-функции могут вызываться только из другой suspend-функции или корутины.
3. Во что компилируются suspend-функции?
При компиляции suspend-функции преобразуются в объект "state machine". Каждая приостановка (например, вызов другой suspend-функции или delay) разбивает код на части, которые выполняются между точками приостановки.
Каждый вызов другой suspend-функции становится точкой приостановки. Компилятор разбивает код на блоки между этими точками и сохраняет:
Текущий label (номер состояния).
Локальные переменные.
Структура скомпилированного кода:
Компилятор добавляет скрытый параметр Continuation (представляет контекст выполнения).
Функция разбивается на блоки, соответствующие участкам кода между точками приостановки.
Для каждого состояния генерируется отдельный case в switch-конструкции.
Сохраняет локальные переменные и текущий label в объекте Continuation.
Пример
suspend fun fetchUserData(): User { val token = fetchToken() // suspend-функция val user = fetchUser(token) // ещё одна suspend-функция return user }
После компиляции (упрощенно):
class FetchStateMachine : Continuation<User> { var label = 0 var token: Token? = null override fun resumeWith(result: Result<User>) { when (label) { 0 -> { label = 1 fetchToken(this) // Передача Continuation } 1 -> { token = result.getOrThrow() label = 2 fetchUser(token, this) } 2 -> { continuation.resumeWith(result) // Возврат результата } } } }
Пример обёртки для асинхронного кода с callback.
Предположим у нас есть callback:
api.download(url, object: NetworkApi.Callback { override fun onSuccess(result: File) { /* use result */ } })
Мы оборачиваем его в suspend функцию, которая результат своей работы должна передать в объект Continuation (который гарантированно имеется в любой suspend функции о чём было выше), чтобы получить доступ к объекту Continuation нужно использовать функцию suspendCoroutine:
suspend fun downloadFile(url: String): File { return suspendCoroutine { сontinuation -> api.download(url, object : NetworkApi.Callback { override fun onSuccess(result: File) { сontinuation.resume(result) } }) } }
Можно также вернуть ошибку:
override fun onFailure(error: Exception) { continuation.resumeWithException(error) }
В случае возврата ошибки код в корутине, который находится ниже suspend функции не будет выполнен.
4. Рекомендации при написании suspend функций:
4.1 Используйте встроенные suspend-функции. Для вызова других suspend-функций применяйте встроенные средства корутин:
Примеры встроенных функций:
delay() — приостанавливает корутину на заданное время.
withContext() — переключает контекст выполнения (например, с UI-потока на фоновый).
coroutineScope() — создает область видимости CoroutineScope, наследующую контекст из запускаемой области, и запускает suspend блок кода в этой области передаваемый в качестве аргумента.
suspend fun loadData() = withContext(Dispatchers.IO) { // Блокирующие операции в фоновом потоке }
4.2 Избегайте блокирующих вызовов:
// Плохо: Thread.sleep(1000) // Блокирует поток // Хорошо: delay(1000) // Приостанавливает корутину
Если представить, что у нас есть синхронная функция загрузки файла:
val file = api.download(url)
И мы хотим как в примере обернуть её в suspend функцию:
// Неправильная suspend функция suspend fun downloadFile(url: String): File { return suspendCoroutine { continuation -> val downloadedFile = networkApi.download(url) continuation.resume(downloadedFile) } }
То мы получим неправильную suspend функцию, которая заблокирует поток корутины, в которой будет вызвана.
Как в этом случае поступить:
suspend fun downloadFile(url: String): File { return withContext(Dispatchers.IO) { networkApi.download(url) } }
4.3 Используйте подходящие диспетчеры:
suspend fun processData() = withContext(Dispatchers.Default) { // Тяжелые вычисления }
4.4 Разделяйте ответственность. Дробите сложные suspend-функции на мелкие, переиспользуемые части:
suspend fun getUser() = fetchUser().also { validate(it) } suspend fun fetchUser(): User { ... } suspend fun validate(user: User) { ... }
4.5 Обрабатывайте исключения. Используйте try/catch или CoroutineExceptionHandler с обработкой ошибок:
suspend fun safeFetch() = try { fetchData() } catch (e: Exception) { "Ошибка: ${e.message}" }
4.6 Учитывайте отмену корутин. Проверяйте статус корутины через isActive или ensureActive():
suspend fun longTask() { repeat(100) { i -> ensureActive() // Проверяет, не отменена ли корутина // Выполняем 1/100 работы } }
4.7 Не забывайте вызов сontinuation.resume(). Если в нашем примере с callback не вызвать сontinuation.resume() корутина зависнет в ожидании вызова от suspend функции и не закончит своё выполнение
suspend fun downloadFile(url: String): File { return suspendCoroutine { сontinuation -> api.download(url, object : NetworkApi.Callback { override fun onSuccess(result: File) { // сontinuation.resume(result) } }) } }
