Побочный эффект — это изменение состояния приложения, которое происходит за пределами области действия компонуемой функции. Из-за жизненного цикла компонуемых объектов и таких свойств, как непредсказуемые перекомпоновки, выполнение перекомпоновок компонуемых объектов в разных порядках или перекомпоновки, которые можно отбросить, компонуемые объекты в идеале должны быть свободны от побочных эффектов .
Однако иногда побочные эффекты необходимы, например, для запуска одноразового события, такого как показ снэк-бара или переход на другой экран при определенном состоянии. Эти действия должны вызываться из контролируемой среды, которая знает о жизненном цикле компонуемого. На этой странице вы узнаете о различных API побочных эффектов, которые предлагает Jetpack Compose.
Варианты использования состояний и эффектов
Как указано в документации Thinking in Compose , компонуемые объекты не должны иметь побочных эффектов. Когда вам нужно внести изменения в состояние приложения (как описано в документации Managing state ), вам следует использовать API Effect, чтобы эти побочные эффекты выполнялись предсказуемым образом .
Из-за различных возможностей, которые эффекты открывают в 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
composable. Использование SideEffect
гарантирует, что эффект будет выполнен после каждой успешной перекомпозиции. С другой стороны, неправильно выполнять эффект до того, как будет гарантирована успешная перекомпозиция, что имеет место при записи эффекта непосредственно в composable.
Например, ваша аналитическая библиотека может позволить вам сегментировать вашу пользовательскую популяцию, прикрепляя пользовательские метаданные («свойства пользователя» в этом примере) ко всем последующим аналитическим событиям. Чтобы сообщить тип текущего пользователя в вашу аналитическую библиотеку, используйте 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
: преобразовать состояние «не составлено» в состояние «составлено»
produceState
запускает сопрограмму, ограниченную Composition, которая может помещать значения в возвращаемое 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>
в холодный Flow. snapshotFlow
запускает свой блок при сборе и выдает результат считанных в нем объектов State
. Когда один из считанных внутри блока snapshotFlow
объектов State
мутирует, 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
, потому что их значение никогда не меняется в Composition из-за использования rememberUpdatedState
. Если вы не передаете lifecycleOwner
как параметр и он изменяется, HomeScreen
перекомпоновывается, но DisposableEffect
не удаляется и не перезапускается. Это вызывает проблемы, потому что с этого момента используется неправильный lifecycleOwner
.
Константы как ключи
Вы можете использовать константу, например true
в качестве ключа эффекта, чтобы он следовал жизненному циклу call site . Для этого есть допустимые варианты использования, например, пример LaunchedEffect
показанный выше. Однако, прежде чем сделать это, подумайте дважды и убедитесь, что это то, что вам нужно.
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Состояние и состав реактивного ранца
- Kotlin для Jetpack Compose
- Использование представлений в Compose