Тема 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)
}
})
}
}