Побочные эффекты в Compose

Побочный эффект — это изменение состояния приложения, которое происходит за пределами компонуемой функции. Из-за жизненного цикла и таких свойств компонуемых объектов, как непредсказуемость рекомпозиций, выполнение рекомпозиций компонуемых объектов в разных порядках или рекомпозиции, которые можно отбросить, в идеале компонуемые объекты не должны иметь побочных эффектов .

Однако иногда побочные эффекты необходимы, например, чтобы вызвать одноразовое событие, такое как отображение закусочной или переход к другому экрану при определенном условии состояния. Эти действия следует вызывать из контролируемой среды, которая знает жизненный цикл компонуемого объекта. На этой странице вы узнаете о различных API-интерфейсах с побочными эффектами, которые предлагает Jetpack Compose.

Варианты использования состояния и эффекта

Как описано в документации Thinking in Compose , составные объекты не должны иметь побочных эффектов. Когда вам нужно внести изменения в состояние приложения (как описано в документе «Управление состоянием документации »), вам следует использовать API-интерфейсы эффектов, чтобы эти побочные эффекты выполнялись предсказуемым образом .

Из-за различных возможностей эффектов, открывающихся в Compose, ими можно легко злоупотребить. Убедитесь, что работа, которую вы в них выполняете, связана с пользовательским интерфейсом и не нарушает однонаправленный поток данных , как описано в документации по управлению состоянием .

LaunchedEffect : запускать функции приостановки в области составного объекта.

Чтобы выполнять работу в течение всего срока службы составного объекта и иметь возможность вызывать функции приостановки, используйте составной объект LaunchedEffect . Когда LaunchedEffect входит в состав композиции, он запускает сопрограмму с блоком кода, передаваемым в качестве параметра. Сопрограмма будет отменена, если LaunchedEffect покинет композицию. Если LaunchedEffect перекомпонован с другими ключами (см. раздел «Перезапуск эффектов» ниже), существующая сопрограмма будет отменена, а новая функция приостановки будет запущена в новой сопрограмме.

Например, вот анимация, которая пульсирует значение альфа с настраиваемой задержкой:

// 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 { mutableStateOf(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)
    }
}

В приведенном выше коде анимация использует delay функции приостановки для ожидания заданного периода времени. Затем он последовательно анимирует альфу до нуля и обратно с помощью animateTo . Это будет повторяться на протяжении всей жизни составного устройства.

rememberCoroutineScope : получить область видимости с учетом композиции для запуска сопрограммы вне компонуемого объекта.

Поскольку LaunchedEffect — это составная функция, ее можно использовать только внутри других составных функций. Чтобы запустить сопрограмму вне составного объекта, но с такой областью действия, чтобы она автоматически отменялась после выхода из композиции, используйте rememberCoroutineScope . Также используйте rememberCoroutineScope всякий раз, когда вам нужно вручную контролировать жизненный цикл одной или нескольких сопрограмм, например, отменяя анимацию при возникновении пользовательского события.

rememberCoroutineScope — это составная функция, которая возвращает CoroutineScope привязанную к точке композиции, где она вызывается. Область будет отменена, когда вызов покинет композицию.

Следуя предыдущему примеру, вы можете использовать этот код для отображения Snackbar , когда пользователь нажимает Button :

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        Column(Modifier.padding(contentPadding)) {
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

rememberUpdatedState : ссылка на значение в эффекте, который не должен перезапускаться, если значение изменится.

LaunchedEffect перезапускается при изменении одного из ключевых параметров. Однако в некоторых ситуациях вам может потребоваться зафиксировать в эффекте значение, при изменении которого эффект не будет перезапускаться. Для этого необходимо использовать rememberUpdatedState для создания ссылки на это значение, которое можно захватить и обновить. Этот подход полезен для эффектов, содержащих долгоживущие операции, воссоздание и перезапуск которых может оказаться дорогостоящим или невозможным.

Например, предположим, что в вашем приложении есть LandingScreen , который через некоторое время исчезает. Даже если LandingScreen перекомпонован, эффект, который ждет некоторое время и уведомляет, что истекшее время, не должен перезапускаться:

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

Чтобы создать эффект, соответствующий жизненному циклу сайта вызова, в качестве параметра передается неизменяемая константа, например Unit или true . В приведенном выше коде используется LaunchedEffect(true) . Чтобы гарантировать, что лямбда-выражение onTimeout всегда содержит последнее значение, с которым был перекомпонован LandingScreen , onTimeout необходимо обернуть функцией rememberUpdatedState . Возвращенное State , currentOnTimeout в коде, должно использоваться в эффекте.

DisposableEffect : эффекты, требующие очистки.

Для побочных эффектов, которые необходимо устранить после изменения ключей или если составной объект покидает композицию, используйте DisposableEffect . Если ключи DisposableEffect изменяются, компонуемому объекту необходимо удалить (выполнить очистку) свой текущий эффект и сбросить его, вызвав эффект еще раз.

Например, вы можете отправлять аналитические события на основе событий Lifecycle с помощью LifecycleObserver . Чтобы прослушивать эти события в Compose, используйте DisposableEffect для регистрации и отмены регистрации наблюдателя при необходимости.

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

В приведенном выше коде эффект добавит observer к lifecycleOwner . Если lifecycleOwner изменяется, эффект удаляется и перезапускается с новым lifecycleOwner .

DisposableEffect должен включать предложение onDispose в качестве последнего оператора в своем блоке кода. В противном случае IDE отображает ошибку времени сборки.

SideEffect : опубликовать состояние Compose для кода, не относящегося к Compose.

Чтобы поделиться состоянием Compose с объектами, не управляемыми Compose, используйте компонуемый SideEffect . Использование SideEffect гарантирует, что эффект будет выполняться после каждой успешной рекомпозиции. С другой стороны, неправильно выполнять эффект до того, как будет гарантирована успешная рекомпозиция, как это происходит при записи эффекта непосредственно в составной элемент.

Например, ваша библиотека аналитики может позволить вам сегментировать популяцию пользователей, присоединяя пользовательские метаданные («свойства пользователя» в этом примере) ко всем последующим событиям аналитики. Чтобы сообщить тип текущего пользователя в вашу библиотеку аналитики, используйте SideEffect для обновления его значения.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState : преобразовать состояние, не являющееся Compose, в состояние Compose.

produceState запускает сопрограмму, ограниченную композицией, которая может помещать значения в возвращаемое State . Используйте его для преобразования состояния, не являющегося Compose, в состояние Compose, например переноса внешнего состояния, управляемого подпиской, такого как Flow , LiveData или RxJava , в Composition.

Производитель запускается, когда produceState входит в состав композиции, и закрывается, когда он покидает композицию. Возвращенное State объединяет; установка того же значения не приведет к повторной композиции.

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

В следующем примере показано, как использовать produceState для загрузки изображения из сети. Составная функция loadNetworkImage возвращает State , которое можно использовать в других составных объектах.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

derivedStateOf : преобразовать один или несколько объектов состояния в другое состояние.

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

Вам следует использовать функцию derivedStateOf когда ваши входные данные для компонуемого объекта меняются чаще, чем вам нужно перекомпоновать. Это часто происходит, когда что-то часто меняется, например, положение прокрутки, но компоновщику нужно реагировать на это только после того, как оно пересекает определенный порог. derivedStateOf создает новый объект состояния Compose, который вы можете наблюдать, который обновляется только настолько, насколько вам нужно. Таким образом, он действует аналогично оператору distinctUntilChanged() в Kotlin Flows.

Правильное использование

В следующем фрагменте показан подходящий вариант использования derivedStateOf :

@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

В этом фрагменте firstVisibleItemIndex изменяется каждый раз при изменении первого видимого элемента. При прокрутке значение становится 0 , 1 , 2 , 3 , 4 , 5 и т. д. Однако рекомпозиция должна произойти только в том случае, если значение больше 0 . Это несоответствие в частоте обновления означает, что это хороший вариант derivedStateOf .

Неправильное использование

Распространенной ошибкой является предположение, что при объединении двух объектов состояния Compose вам следует использовать derivedStateOf , потому что вы «получаете состояние». Однако это чисто накладные расходы и не требуется, как показано в следующем фрагменте:

// DO NOT USE. Incorrect usage of derivedStateOf.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct

В этом фрагменте fullName необходимо обновлять так же часто, как firstName и lastName . Таким образом, никакой лишней рекомпозиции не происходит, и использование derivedStateOf не требуется.

snapshotFlow : преобразовать состояние Compose в потоки

Используйте snapshotFlow для преобразования объектов State<T> в холодный поток. snapshotFlow запускает свой блок при сборе и выдает результат чтения объектов State . Когда один из объектов State , прочитанных внутри блока snapshotFlow мутирует, Flow отправит новое значение своему сборщику, если новое значение не равно предыдущему выданному значению (это поведение аналогично поведению Flow.distinctUntilChanged ).

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

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

В приведенном выше коде listState.firstVisibleItemIndex преобразуется в Flow, который может извлечь выгоду из возможностей операторов Flow.

Перезапуск эффектов

Некоторые эффекты в Compose, такие как LaunchedEffect , produceState или DisposableEffect , принимают переменное количество аргументов, ключей, которые используются для отмены текущего эффекта и запуска нового с новыми ключами.

Типичная форма этих API:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

Из-за тонкостей такого поведения могут возникнуть проблемы, если параметры, используемые для перезапуска эффекта, не являются правильными:

  • Перезапуск эффектов меньше, чем должен, может привести к ошибкам в вашем приложении.
  • Перезапуск влияет больше, чем должен, может быть неэффективным.

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

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

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // These values never change in Composition
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            /* ... */
        }

        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

currentOnStart и currentOnStop не нужны в качестве ключей DisposableEffect , поскольку их значение никогда не меняется в композиции из-за использования rememberUpdatedState . Если вы не передадите lifecycleOwner в качестве параметра и он изменится, HomeScreen выполнит перекомпоновку, но DisposableEffect не будет удален и перезапущен. Это вызывает проблемы, поскольку с этого момента используется неправильный lifecycleOwner .

Константы как ключи

Вы можете использовать константу, например true в качестве ключа эффекта, чтобы она соответствовала жизненному циклу сайта вызова . Для этого существуют допустимые варианты использования, например, пример LaunchedEffect , показанный выше. Однако, прежде чем сделать это, подумайте дважды и убедитесь, что это то, что вам нужно.

{% дословно %} {% дословно %} {% дословно %} {% дословно %}