Сопрограмма — это шаблон проектирования параллельного программирования, который можно использовать в 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, входящий в состав расширений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 */ } } } } }
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
withContextblock finishes, the coroutine inlogin()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:
- Обзор сопрограмм (JetBrains)
- Руководство по сопрограммам (JetBrains)
- Additional resources for Kotlin coroutines and flow