Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров1.2K

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

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+4
Комментарии0

Публикации

Работа

Ближайшие события