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

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

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

Функции

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

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

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

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

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

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

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

Чтобы использовать сопрограммы в вашем Android-проекте, добавьте следующую зависимость в файл build.gradle вашего приложения:

классный

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)
        }
    }
}

Давайте разберем код сопрограмм в функции login :

  • 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 */ }
            }
        }
    }
}

Обратите внимание, что сопрограмма здесь по-прежнему необходима, поскольку makeLoginRequest — это функция suspend , а все функции suspend должны выполняться в сопрограмме.

Этот код отличается от предыдущего примера login несколькими способами:

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

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

  • Приложение вызывает функцию login() из слоя View в основном потоке.
  • launch создает новую сопрограмму в основном потоке, и эта сопрограмма начинает выполнение.
  • Внутри сопрограммы вызов loginRepository.makeLoginRequest() теперь приостанавливает дальнейшее выполнение сопрограммы до тех пор, пока не завершится выполнение блока withContext в makeLoginRequest() .
  • После завершения блока withContext сопрограмма в login() возобновляет выполнение в основном потоке с результатом сетевого запроса.

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

Для обработки исключений, которые может генерировать слой 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() обрабатывается в пользовательском интерфейсе как ошибка.

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

Для более подробного ознакомления с сопрограммами на Android см. статью «Повышение производительности приложения с помощью сопрограмм Kotlin» .

Дополнительные материалы по сопрограммам можно найти по следующим ссылкам: