Сопрограмма — это шаблон проектирования параллелизма, который можно использовать в Android для упрощения кода, выполняющегося асинхронно. Сопрограммы были добавлены в Kotlin в версии 1.3 и основаны на устоявшихся концепциях других языков.
В Android сопрограммы помогают управлять долго выполняющимися задачами, которые в противном случае могут заблокировать основной поток и привести к тому, что ваше приложение перестанет отвечать на запросы. Более 50% профессиональных разработчиков, использующих сопрограммы, сообщили о повышении производительности. В этом разделе описывается, как можно использовать сопрограммы Kotlin для решения этих проблем, позволяя писать более чистый и лаконичный код приложения.
Функции
Coroutines — наше рекомендуемое решение для асинхронного программирования на Android. Примечательные особенности включают следующее:
- Легкость : вы можете запускать множество сопрограмм в одном потоке благодаря поддержке приостановки , которая не блокирует поток, в котором выполняется сопрограмма. Приостановка экономит память по сравнению с блокировкой, одновременно поддерживая множество одновременных операций.
- Меньше утечек памяти . Используйте структурированный параллелизм для выполнения операций в определенной области.
- Встроенная поддержка отмены : отмена распространяется автоматически через действующую иерархию сопрограмм.
- Интеграция с Jetpack . Многие библиотеки Jetpack включают расширения , обеспечивающие полную поддержку сопрограмм. Некоторые библиотеки также предоставляют собственную область сопрограмм , которую можно использовать для структурированного параллелизма.
Обзор примеров
На основе Руководства по архитектуре приложений примеры в этом разделе выполняют сетевой запрос и возвращают результат в основной поток, где приложение затем может отобразить результат пользователю.
В частности, компонент ViewModel
Architecture вызывает уровень репозитория в основном потоке для запуска сетевого запроса. В этом руководстве рассматриваются различные решения, использующие сопрограммы для разблокировки основного потока.
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>() } 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 .
Дополнительные ресурсы по сопрограммам можно найти по следующим ссылкам:
- Обзор сопрограмм (JetBrains)
- Руководство по сопрограммам (JetBrains)
- Дополнительные ресурсы для сопрограмм и потоков Kotlin