Pull to refresh

Шпаргалка: Kotlin suspend функции

Level of difficultyEasy
Reading time4 min
Views1.8K

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

Tags:
Hubs:
Total votes 2: ↑2 and ↓0+4
Comments0

Articles