Сопрограммы Kotlin предоставляют API, который позволяет писать асинхронный код. С помощью сопрограмм Kotlin вы можете определить CoroutineScope
, который поможет вам управлять тем, когда ваши сопрограммы должны запускаться. Каждая асинхронная операция выполняется в определенной области.
Компоненты с учетом жизненного цикла обеспечивают первоклассную поддержку сопрограмм для логических областей вашего приложения, а также уровень взаимодействия с LiveData
. В этом разделе объясняется, как эффективно использовать сопрограммы с компонентами, учитывающими жизненный цикл.
Добавить зависимости KTX
Встроенные области сопрограмм, описанные в этом разделе, содержатся в расширениях KTX для каждого соответствующего компонента. Обязательно добавьте соответствующие зависимости при использовании этих областей.
- Для
ViewModelScope
используйтеandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
или более позднюю версию. - Для
LifecycleScope
используйтеandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
или более позднюю версию. - Для
liveData
используйтеandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0
или более позднюю версию.
Области сопрограмм с учетом жизненного цикла
Компоненты с учетом жизненного цикла определяют следующие встроенные области, которые вы можете использовать в своем приложении.
ViewModelScope
ViewModelScope
определяется для каждой ViewModel
в вашем приложении. Любая сопрограмма, запущенная в этой области, автоматически отменяется, если ViewModel
очищается. Сопрограммы здесь полезны, когда у вас есть работа, которую необходимо выполнить, только если ViewModel
активна. Например, если вы вычисляете некоторые данные для макета, вам следует ограничить работу ViewModel
, чтобы в случае очистки ViewModel
работа автоматически отменялась во избежание потребления ресурсов.
Вы можете получить доступ к CoroutineScope
ViewModel
через свойство viewModelScope
ViewModel, как показано в следующем примере:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
Область жизненного цикла
LifecycleScope
определяется для каждого объекта Lifecycle
. Любая сопрограмма, запущенная в этой области, отменяется при уничтожении Lifecycle
. Доступ к CoroutineScope
Lifecycle
можно получить либо через свойства lifecycle.coroutineScope
или lifecycleOwner.lifecycleScope
.
В приведенном ниже примере показано, как использовать lifecycleOwner.lifecycleScope
для асинхронного создания предварительно вычисленного текста:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
Перезапускаемые сопрограммы с учетом жизненного цикла
Несмотря на то, что lifecycleScope
предоставляет правильный способ автоматической отмены длительных операций, когда Lifecycle
DESTROYED
, у вас могут быть другие случаи, когда вы хотите начать выполнение блока кода, когда Lifecycle
находится в определенном состоянии, и отменить, когда он находится в определенном состоянии. находится в другом штате. Например, вы можете захотеть собрать поток, когда Lifecycle
STARTED
и отменить сбор, когда он STOPPED
. Этот подход обрабатывает выбросы потока только тогда, когда пользовательский интерфейс виден на экране, что экономит ресурсы и потенциально позволяет избежать сбоев приложения.
Для этих случаев Lifecycle
и LifecycleOwner
предоставляют API приостановки repeatOnLifecycle
, который делает именно это. В следующем примере содержится блок кода, который запускается каждый раз, когда соответствующий Lifecycle
находится хотя бы в состоянии STARTED
, и отменяется, когда Lifecycle
STOPPED
:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
Сбор данных о потоках с учетом жизненного цикла
Если вам нужно выполнить сбор данных с учетом жизненного цикла только в одном потоке, вы можете использовать метод Flow.flowWithLifecycle()
чтобы упростить код:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
Однако если вам необходимо выполнить сбор с учетом жизненного цикла для нескольких потоков параллельно, вам необходимо собирать каждый поток в разных сопрограммах. В этом случае более эффективно использовать repeatOnLifecycle()
напрямую:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Because collect is a suspend function, if you want to
// collect multiple flows in parallel, you need to do so in
// different coroutines.
launch {
flow1.collect { /* Process the value. */ }
}
launch {
flow2.collect { /* Process the value. */ }
}
}
}
Приостановить сопрограммы с учетом жизненного цикла
Несмотря на то, что CoroutineScope
предоставляет правильный способ автоматической отмены длительных операций, у вас могут быть другие случаи, когда вы захотите приостановить выполнение блока кода, если Lifecycle
не находится в определенном состоянии. Например, чтобы запустить FragmentTransaction
, вам необходимо дождаться, пока Lifecycle
не станет хотя бы STARTED
. Для этих случаев Lifecycle
предоставляет дополнительные методы: lifecycle.whenCreated
, lifecycle.whenStarted
и lifecycle.whenResumed
. Любой запуск сопрограммы внутри этих блоков приостанавливается, если Lifecycle
не находится хотя бы в минимально желаемом состоянии.
В приведенном ниже примере содержится блок кода, который запускается только тогда, когда соответствующий Lifecycle
находится как минимум в состоянии STARTED
:
class MyFragment: Fragment {
init { // Notice that we can safely launch in the constructor of the Fragment.
lifecycleScope.launch {
whenStarted {
// The block inside will run only when Lifecycle is at least STARTED.
// It will start executing when fragment is started and
// can call other suspend methods.
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
Если Lifecycle
уничтожается, когда сопрограмма активна с помощью одного when
методов, сопрограмма автоматически отменяется. В приведенном ниже примере блок finally
запускается, как только состояние Lifecycle
становится DESTROYED
:
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
Используйте сопрограммы с LiveData
При использовании LiveData
вам может потребоваться асинхронное вычисление значений. Например, вы можете захотеть получить настройки пользователя и передать их в свой пользовательский интерфейс. В этих случаях вы можете использовать функцию построения liveData
для вызова функции suspend
, предоставляя результат в виде объекта LiveData
.
В приведенном ниже примере loadUser()
— это функция приостановки, объявленная в другом месте. Используйте функцию компоновщика liveData
для асинхронного вызова loadUser()
, а затем используйте emit()
для выдачи результата:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
Строительный блок liveData
служит структурированным примитивом параллелизма между сопрограммами и LiveData
. Блок кода начинает выполняться, когда LiveData
становится активным, и автоматически отменяется по истечении настраиваемого таймаута, когда LiveData
становится неактивным. Если он отменен до завершения, он перезапускается, если LiveData
снова становится активным. Если предыдущий запуск завершился успешно, он не перезапускается. Обратите внимание, что он перезапускается только в случае автоматической отмены. Если блок отменяется по какой-либо другой причине (например, при возникновении CancellationException
), он не перезапускается.
Вы также можете выдать несколько значений из блока. Каждый вызов emit()
приостанавливает выполнение блока до тех пор, пока значение LiveData
не будет установлено в основном потоке.
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
Вы также можете объединить liveData
с Transformations
, как показано в следующем примере:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
Вы можете выдать несколько значений из LiveData
, вызывая функцию emitSource()
каждый раз, когда хотите выдать новое значение. Обратите внимание, что каждый вызов метода emit()
или emitSource()
удаляет ранее добавленный источник.
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
Дополнительную информацию о сопрограммах можно найти по следующим ссылкам:
- Улучшите производительность приложения с помощью сопрограмм Kotlin
- Обзор сопрограмм
- Потоки в CoroutineWorker
Дополнительные ресурсы
Чтобы узнать больше об использовании сопрограмм с компонентами, учитывающими жизненный цикл, обратитесь к следующим дополнительным ресурсам.
Образцы
Блоги
- Сопрограммы на Android: шаблоны приложений
- Простые сопрограммы в Android: viewModelScope
- Тестирование двух последовательных выбросов LiveData в сопрограммах
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Обзор LiveData
- Управление жизненными циклами с помощью компонентов, учитывающих жизненный цикл
- Загрузка и отображение постраничных данных