Побочный эффект — это изменение состояния приложения, происходящее вне области действия компонуемой функции. Ввиду жизненного цикла компонуемых объектов и таких свойств, как непредсказуемые рекомпозиции, выполнение рекомпозиций компонуемых объектов в различном порядке или возможность отмены рекомпозиций, в идеале компонуемые объекты должны быть свободны от побочных эффектов .
Однако иногда необходимы побочные эффекты, например, для запуска разового события, такого как отображение всплывающего уведомления или переход на другой экран при заданном состоянии. Эти действия следует вызывать из контролируемой среды, которая учитывает жизненный цикл компонуемого объекта. На этой странице вы узнаете о различных API для обработки побочных эффектов, которые предлагает Jetpack 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 { 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) } }
В приведенном выше коде анимация использует функцию задержки (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 запускает сопрограмму, ограниченную областью видимости композиции, которая может передавать значения в возвращаемое State . Используйте его для преобразования состояния, не относящегося к композиции, в состояние композиции, например, для переноса внешнего состояния, управляемого подпиской, такого как 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 в потоки (Flows).
Используйте snapshotFlow для преобразования объектов State<T> в «холодный» поток. snapshotFlow запускает свой блок при сборке мусора и выдает результат обработки считанных из него объектов State . Когда один из считанных внутри блока snapshotFlow объектов State изменяется, поток выдаст новое значение своему сборщику мусора, если это новое значение не равно предыдущему выданному значению (это поведение аналогично поведению 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 and Jetpack Compose
- Kotlin для Jetpack Compose
- Использование представлений в Compose