Как использовать сопрограммы 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-функции. Таким образом, когда composable-функция покидает экран (выходит из композиции), все запущенные сопрограммы автоматически отменяются, чтобы предотвратить утечку памяти.

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

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

  • Вход. Сопрограмма запускается, когда 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-функции.

@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()

    // ...
}

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

Если вам нужно рассчитать значения асинхронно, используйте 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…

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

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

Образцы