Побочный эффект — это изменение состояния приложения, происходящее вне области действия компонуемой функции. Ввиду жизненного цикла компонуемых объектов и таких свойств, как непредсказуемые перекомпозиции, выполнение перекомпозиций компонуемых объектов в разном порядке или перекомпозиции, которые можно отменить, компонуемые объекты в идеале должны быть лишены побочных эффектов .
Однако иногда побочные эффекты необходимы, например, для запуска одноразового события, такого как отображение снэк-бара или переход на другой экран при определённом состоянии. Эти действия следует вызывать из контролируемой среды, которая учитывает жизненный цикл компонуемого объекта. На этой странице вы узнаете о различных 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) } }
В приведённом выше коде анимация использует функцию задержки (suspending function 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
запускает сопрограмму, областью действия которой является Composition, которая может передавать значения в возвращаемое State
. Используйте её для преобразования состояния, отличного от Compose, в состояние Compose, например, для добавления в Composition состояния, управляемого внешней подпиской, такого как Flow
, LiveData
или RxJava
.
Производитель запускается, когда 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
. При мутации одного из объектов 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
, поскольку их значение никогда не меняется в Composition благодаря использованию rememberUpdatedState
. Если вы не передадите lifecycleOwner
в качестве параметра, а он изменится, HomeScreen
выполнит повторную композицию, но DisposableEffect
не будет удален и перезапущен. Это создаст проблемы, поскольку с этого момента будет использоваться неверный lifecycleOwner
.
Константы как ключи
Вы можете использовать константу, например, true
в качестве ключа эффекта, чтобы он следовал жизненному циклу объекта вызова . Существуют допустимые примеры использования, например, пример LaunchedEffect
, показанный выше. Однако, прежде чем сделать это, дважды подумайте и убедитесь, что это именно то, что вам нужно.
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- State и Jetpack Compose
- Kotlin для Jetpack Compose
- Использование представлений в Compose