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