Сопрограмма — это шаблон проектирования параллелизма, который можно использовать в 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
Сопрограмма — это шаблон проектирования параллелизма, который можно использовать в 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