Корутины Kotlin на Android

Сопрограмма — это шаблон проектирования параллельного программирования, который можно использовать в Android для упрощения кода, выполняемого асинхронно. Сопрограммы были добавлены в Kotlin в версии 1.3 и основаны на устоявшихся концепциях из других языков.

В Android сопрограммы помогают управлять длительными задачами, которые в противном случае могли бы блокировать основной поток и привести к зависанию приложения. Более 50% профессиональных разработчиков, использующих сопрограммы, отмечают повышение производительности. В этой статье описывается, как использовать сопрограммы Kotlin для решения этих проблем, что позволит вам писать более чистый и лаконичный код приложения.

Функции

Корутины — это рекомендуемое нами решение для асинхронного программирования на Android. К числу важных особенностей относятся следующие:

  • Легковесность : Благодаря поддержке приостановки (suspend) , которая не блокирует поток, в котором выполняется сопрограмма, можно запускать множество сопрограмм в одном потоке. Приостановка экономит память по сравнению с блокировкой, поддерживая при этом множество параллельных операций.
  • Меньше утечек памяти : используйте структурированную параллельность для выполнения операций в пределах заданной области видимости.
  • Встроенная поддержка отмены : отмена автоматически распространяется по всей иерархии запущенных сопрограмм.
  • Интеграция с Jetpack : Многие библиотеки Jetpack включают расширения , обеспечивающие полную поддержку сопрограмм. Некоторые библиотеки также предоставляют собственную область видимости сопрограмм , которую можно использовать для структурированного параллельного выполнения.

Обзор примеров

В соответствии с руководством по архитектуре приложений , примеры в этом разделе выполняют сетевой запрос и возвращают результат в основной поток, где приложение затем может отобразить результат пользователю.

В частности, компонент архитектуры ViewModel вызывает слой репозитория в основном потоке для запуска сетевого запроса. В этом руководстве рассматриваются различные решения, использующие сопрограммы для разблокировки основного потока.

ViewModel включает в себя набор расширений KTX, которые работают непосредственно с сопрограммами. Эти расширения представляют собой библиотеку lifecycle-viewmodel-ktx и используются в данном руководстве.

Информация о зависимостях

To use coroutines in your Android project, add the following dependency to your app's build.gradle file:

классный

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Котлин

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Выполнение в фоновом потоке.

Отправка сетевого запроса в основном потоке приводит к тому, что он ожидает или блокируется до получения ответа. Поскольку поток заблокирован, операционная система не может вызвать onDraw() , что приводит к зависанию приложения и потенциально может вызвать диалоговое окно "Приложение не отвечает" (ANR). Для улучшения пользовательского опыта давайте выполним эту операцию в фоновом потоке.

Для начала давайте взглянем на наш класс Repository и посмотрим, как он выполняет сетевой запрос:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

private const val loginUrl = "https://example.com/login"

class LoginRepository(private val responseParser: LoginResponseParser) {
    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest является синхронным и блокирует вызывающий поток. Для моделирования ответа на сетевой запрос у нас есть собственный класс Result .

ViewModel инициирует сетевой запрос, когда пользователь, например, нажимает на кнопку:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

В предыдущем коде LoginViewModel блокирует поток пользовательского интерфейса при выполнении сетевого запроса. Простейшее решение для переноса выполнения запроса из основного потока — создать новую сопрограмму и выполнить сетевой запрос в отдельном потоке ввода-вывода:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Let's dissect the coroutines code in the login function:

  • viewModelScope — это предопределенный CoroutineScope , входящий в состав расширений ViewModel KTX. Обратите внимание, что все сопрограммы должны выполняться в определенной области видимости. CoroutineScope управляет одной или несколькими связанными сопрограммами.
  • Функция launch создает сопрограмму и передает выполнение ее тела соответствующему диспетчеру.
  • Dispatchers.IO указывает, что эта сопрограмма должна выполняться в потоке, зарезервированном для операций ввода-вывода.

Функция login выполняется следующим образом:

  • Приложение вызывает функцию login из слоя View в основном потоке.
  • launch создается новая сопрограмма, а сетевой запрос выполняется независимо в потоке, зарезервированном для операций ввода-вывода.
  • Пока выполняется сопрограмма, функция login продолжает выполнение и возвращается, возможно, до завершения сетевого запроса. Обратите внимание, что для простоты сетевой ответ пока игнорируется.

Поскольку эта сопрограмма запускается с помощью viewModelScope , она выполняется в области видимости ViewModel . Если ViewModel уничтожается из-за того, что пользователь покидает экран, viewModelScope автоматически отменяется, и все запущенные сопрограммы также отменяются.

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

Используйте сопрограммы для обеспечения безопасности основного кода.

Функция считается безопасной для основного потока, если она не блокирует обновления пользовательского интерфейса в основном потоке. Функция makeLoginRequest не является безопасной для основного потока, поскольку вызов makeLoginRequest из основного потока блокирует пользовательский интерфейс. Используйте функцию withContext() из библиотеки сопрограмм, чтобы перенести выполнение сопрограммы в другой поток:

class LoginRepository(
    // ...
) {
    // ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) переносит выполнение сопрограммы в поток ввода-вывода, что делает вызывающую функцию безопасной для основного потока и позволяет пользовательскому интерфейсу обновляться по мере необходимости.

makeLoginRequest также помечена ключевым словом suspend . Это ключевое слово Kotlin используется для принудительного вызова функции из сопрограммы.

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

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> { /* Happy path */ }
                else -> { /* Show error in UI */ }
            }
        }
    }
}

Note that the coroutine is still needed here, since makeLoginRequest is a suspend function, and all suspend functions must be executed in a coroutine.

This code differs from the previous login example in a couple of ways:

  • launch не принимает параметр Dispatchers.IO . Если вы не передаете Dispatcher в launch , любые сопрограммы, запущенные из viewModelScope выполняются в основном потоке.
  • Результат сетевого запроса теперь обрабатывается таким образом, что отображается пользовательский интерфейс, сообщающий об успехе или неудаче.

Функция авторизации теперь выполняется следующим образом:

  • Приложение вызывает функцию login() из слоя View в основном потоке.
  • launch создает новую сопрограмму в основном потоке, и эта сопрограмма начинает выполнение.
  • Внутри сопрограммы вызов loginRepository.makeLoginRequest() теперь приостанавливает дальнейшее выполнение сопрограммы до тех пор, пока не завершится выполнение блока withContext в makeLoginRequest() .
  • Once the withContext block finishes, the coroutine in login() resumes execution on the main thread with the result of the network request.

Обработка исключений

Для обработки исключений, которые может генерировать слой Repository , используйте встроенную в Kotlin поддержку исключений . В следующем примере мы используем блок try-catch :

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch (e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> { /* Happy path */ }
                else -> { /* Show error in UI */ }
            }
        }
    }
}

В этом примере любое неожиданное исключение, возникающее при вызове функции makeLoginRequest() обрабатывается в пользовательском интерфейсе как ошибка.

Дополнительные ресурсы сопрограмм

For a more detailed look at coroutines on Android, see Improve app performance with Kotlin coroutines .

For more coroutines resources, see the following links: