Как использовать сопрограммы Kotlin с компонентами, учитывающими жизненный цикл

Корутины Kotlin предоставляют API, который позволяет писать асинхронный код. С помощью сопрограмм Kotlin можно задать CoroutineScope, который позволяет управлять временем выполнения сопрограмм. Каждая асинхронная операция выполняется в определенной области действия.

Компоненты с учетом жизненного цикла обеспечивают первоклассную поддержку сопрограмм для логических областей в приложении. В этом документе рассказывается, как эффективно использовать сопрограммы с компонентами с учетом жизненного цикла.

Добавьте зависимости

Встроенные области действия сопрограмм, описанные в этом разделе, содержатся в API жизненного цикла. При использовании этих областей обязательно добавьте необходимые зависимости.

  • Для утилит ViewModel в Compose используйте implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version").
  • Для утилит жизненного цикла в Compose используйте implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version").

Области действия сопрограмм с учетом жизненного цикла

Библиотеки Compose и Lifecycle предоставляют следующие встроенные области действия, которые можно использовать в приложении:

ViewModelScope

Для каждого ViewModel в приложении определяется ViewModelScope. Любая сопрограмма, запущенная в этой области действия, автоматически отменяется, если ViewModel удаляется. Корутины полезны, когда нужно выполнить определенную работу, только если ViewModel активен. Например, если вы обрабатываете данные для макета, задайте область действия для ViewModel, чтобы при очистке ViewModel обработка автоматически отменялась и не расходовала ресурсы.

Вы можете получить доступ к CoroutineScope объекта ViewModel через свойство viewModelScope объекта ViewModel, как показано в следующем примере:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

В более сложных случаях вы можете передать пользовательский объект CoroutineScope непосредственно в конструктор ViewModel, чтобы заменить объект viewModelScope по умолчанию. Этот подход обеспечивает больше контроля и гибкости, особенно в следующих случаях:

  • Тестирование. Позволяет внедрить TestScope, чтобы было проще контролировать время и проверять поведение сопрограмм в модульных тестах.

  • Специальная конфигурация. Вы можете настроить область действия с помощью определенного CoroutineDispatcher (например, Dispatchers.Default для ресурсоемких вычислений) или специального CoroutineExceptionHandler до того, как ViewModel начнет работу.

Области действия, связанные с композицией

Побочные эффекты, такие как анимация, сетевые вызовы или таймеры, должны быть ограничены жизненным циклом composable-функции. Таким образом, когда компонент покидает экран (выходит из композиции), все запущенные сопрограммы автоматически отменяются, чтобы предотвратить утечки памяти.

Compose предоставляет API LaunchedEffect для декларативного управления областью действия композиции.

LaunchedEffect создает CoroutineScope, позволяющий выполнять функции приостановки. Область действия связана с жизненным циклом композиции composable-функции, а не с жизненным циклом объекта Activity, в котором она размещена.

  • Enter: сопрограмма запускается, когда composable-функция входит в композицию.
  • Выход: сопрограмма отменяется, когда composable-функция покидает композицию.
  • Перезапуск. Если какой-либо из ключей, переданных в LaunchedEffect, изменится, существующая сопрограмма будет отменена и запустится новая.

В следующем примере показано, как использовать LaunchedEffect для создания пульсирующей анимации. Сопрограмма связана с присутствием composable-функции в композиции и реагирует на изменения конфигурации:

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableLongStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

Подробнее о функции LaunchedEffect рассказывается в статье Побочные эффекты в Compose.

Сбор данных о потоках с учетом жизненного цикла

Чтобы безопасно собирать потоки в Jetpack Compose, используйте API collectAsStateWithLifecycle. Эта функция преобразует Flow в объект Compose State и автоматически управляет подпиской на жизненный цикл. По умолчанию сбор данных начинается, когда жизненный цикл имеет значение STARTED, и заканчивается, когда он имеет значение STOPPED. Чтобы переопределить это поведение по умолчанию, передайте параметр minActiveState с нужным методом жизненного цикла, например Lifecycle.State.RESUMED.

В примере ниже показано, как собрать StateFlow из ViewModel в функции, поддерживающей композицию:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Параллельный сбор нескольких потоков

В Compose можно собирать несколько потоков параллельно, объявив несколько переменных состояния. Поскольку collectAsStateWithLifecycle управляет собственной областью действия, параллельный сбор данных выполняется автоматически:

@Composable
fun DashboardScreen(viewModel: DashboardViewModel = viewModel()) {
    // Both flows are collected safely in parallel and will emit updates when either changes, the composables will recompose
    val userData by viewModel.userFlow.collectAsStateWithLifecycle()
    val feedData by viewModel.feedFlow.collectAsStateWithLifecycle()

    // ...
}

Как асинхронно вычислять значения с помощью потоков

Если вам нужно рассчитать значения асинхронно, используйте StateFlow с оператором stateIn.

В приведенном ниже фрагменте кода стандартный объект Flow преобразуется в объект StateFlow. Параметр WhileSubscribed(5000) позволяет подписке оставаться активной в течение пяти секунд после исчезновения интерфейса, чтобы обработать изменения конфигурации.

val uiState: StateFlow<Result> = flow {
    emit(repository.fetchData())
}
.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = Result.Loading
)

Используйте collectAsStateWithLifecycle, чтобы преобразовать собранные значения в Compose State, чтобы интерфейс мог реагировать на изменения данных.

Подробнее о состоянии и Jetpack Compose…

Дополнительные ресурсы

Просмотр контента

Образцы