Сопрограммы Kotlin на Android,Сопрограммы Kotlin на Android

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

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

Функции

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

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

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

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

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

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

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

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

Groovy

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

Kotlin

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

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // 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 . Это ключевое слово — способ Котлина обеспечить вызов функции из сопрограммы.

В следующем примере сопрограмма создается в 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 .

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