Побочный эффект — это изменение состояния приложения, которое происходит за пределами компонуемой функции. Из-за жизненного цикла и таких свойств компонуемых объектов, как непредсказуемость рекомпозиций, выполнение рекомпозиций компонуемых объектов в разных порядках или рекомпозиций, которые можно отбросить, в идеале компонуемые объекты не должны иметь побочных эффектов .
Однако иногда необходимы побочные эффекты, например, чтобы вызвать одноразовое событие, такое как отображение закусочной или переход к другому экрану при определенном состоянии состояния. Эти действия следует вызывать из контролируемой среды, которая знает жизненный цикл компонуемого объекта. На этой странице вы узнаете о различных 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
, показанный выше. Однако, прежде чем сделать это, подумайте дважды и убедитесь, что это то, что вам нужно.
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- State и Jetpack Compose
- Котлин для Jetpack Compose
- Использование представлений в Compose