Um efeito colateral é uma mudança no estado do app que acontece fora do escopo de uma função combinável. Devido ao ciclo de vida e às propriedades das funções de composição (como as recomposições imprevisíveis, a execução de recomposições de elementos de composição em diferentes ordens ou as recomposições que podem ser descartadas), elas precisam ser livres de efeitos colaterais.
No entanto, às vezes, os efeitos colaterais são necessários. Por exemplo, para acionar um evento único a fim de mostrar uma snackbar ou para navegar para outra tela devido a determinada condição de estado. Essas ações precisam ser chamadas em um ambiente controlado, compatível com o ciclo de vida das funções de composição. Nesta página, você vai aprender sobre as diferentes APIs de efeitos colaterais oferecidas pelo Jetpack Compose.
Casos de uso de estado e efeito
Conforme visto na documentação Trabalhando com o Compose, as funções que podem ser compostas precisam ser livres de efeitos colaterais. Quando você precisar fazer mudanças no estado do app, conforme descrito no documento Como gerenciar a documentação sobre os estados, use as APIs Effect para que esses efeitos colaterais sejam executados de forma previsível.
Devido às diferentes possibilidades oferecidas por efeitos no Compose, eles podem ser usados de forma excessiva. Confira se o trabalho que você realiza nesses efeitos é relacionado à IU e não quebra o fluxo de dados unidirecional, conforme explicado no artigo Como gerenciar a documentação sobre os estados.
LaunchedEffect: executar funções de suspensão no escopo de uma função que pode ser composta
Para realizar o trabalho durante a vida útil de uma função que pode ser composta e ter a capacidade de chamar
funções de suspensão, use o
LaunchedEffect
elemento combinável. Quando LaunchedEffect entra na composição, ele inicia uma corrotina com o bloco de código transmitido como um parâmetro. A corrotina será
cancelada se LaunchedEffect sair da composição. Se LaunchedEffect for
recomposto com chaves diferentes (consulte a seção Como reiniciar
efeitos abaixo), a corrotina existente será
cancelada e a nova função de suspensão será iniciada em uma nova corrotina.
Por exemplo, aqui está uma animação que pulsa o valor Alfa com um atraso configurável:
// 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) } }
No código acima, a animação usa a função de suspensão
delay
para aguardar o período definido. Em seguida, ela anima sequencialmente o alfa
para zero e volta a usar
animateTo.
Isso será repetido durante a vida útil da função que pode ser composta.
rememberCoroutineScope: extrair um escopo compatível com a composição para iniciar uma corrotina fora de uma função que pode ser composta
Como LaunchedEffect é uma função combinável, ela só pode ser usada dentro de outras funções combináveis. Para iniciar uma corrotina fora de uma função que pode ser composta,
mas com escopo para que ela seja automaticamente cancelada ao
sair da composição, use
rememberCoroutineScope.
Além disso, use rememberCoroutineScope sempre que precisar controlar o ciclo de vida de
uma ou mais corrotinas manualmente, por exemplo, para cancelar uma animação quando um
evento de usuário ocorrer.
A rememberCoroutineScope é uma função que pode ser composta que retorna um
CoroutineScope vinculado ao ponto da composição em que ele é chamado. O
escopo será cancelado quando a chamada sair da composição.
Seguindo o exemplo anterior, esse código poderia ser usado para exibir uma Snackbar quando o usuário toca em um 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: referenciar um valor em um efeito que não pode ser reiniciado se o valor mudar
O LaunchedEffect é reiniciado quando um dos parâmetros de chave muda. No entanto, em
algumas situações, você pode querer capturar um valor que, se modificado, faria com que o efeito fosse reiniciado,
e é possível que você não queira que isso aconteça. Para fazer isso, é
necessário usar rememberUpdatedState para criar uma referência a esse valor que
possa ser capturada e atualizada. Essa abordagem é útil para efeitos que contêm
operações de longa duração que podem ser caras ou proibitivas para recriar e
reiniciar.
Por exemplo, suponha que o app tenha uma LandingScreen que desaparece após determinado
período. Mesmo que LandingScreen seja recomposta, o efeito que aguarda por um determinado período
e avisa que o período passou não precisa ser reiniciado:
@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 */ }
Para criar um efeito que corresponda ao ciclo de vida do local de chamada,
uma constante que nunca muda (como Unit ou true) é transmitida como parâmetro. No
código acima, LaunchedEffect(true) é usado. Para garantir que a lambda onTimeout
sempre contenha o valor mais recente usado para recompor a LandingScreen,
onTimeout precisa ser unido à função rememberUpdatedState.
O State retornado, currentOnTimeout no código, será usado no
efeito.
DisposableEffect: efeitos que exigem limpeza
Para efeitos colaterais que exigem a limpeza depois que as chaves mudam ou se a
função que pode ser composta sai da composição, use
DisposableEffect.
Se as chaves DisposableEffect mudarem, a função que pode ser composta precisa descartar o
efeito atual, ou seja, fazer a limpeza, e ser redefinida chamando o efeito novamente.
Por exemplo, pode ser necessário enviar eventos de análise com base em
eventos do Lifecycle
usando um
LifecycleObserver.
Para detectar esses eventos no Compose, use um DisposableEffect para registrar e
cancelar o registro do observador quando necessário.
@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 */ }
No código acima, o efeito adicionará o observer ao
lifecycleOwner. Se o lifecycleOwner mudar, o efeito será descartado e
reiniciado com o novo lifecycleOwner.
Um DisposableEffect precisa incluir uma cláusula onDispose como a instrução
final no bloco de código. Caso contrário, o ambiente de desenvolvimento integrado exibirá um erro de tempo de compilação.
SideEffect: publicar estado do Compose em código que não é do Compose
Para compartilhar o estado do Compose com objetos não gerenciados pelo Compose, use o
SideEffect
elemento combinável. O uso de um SideEffect garante que o efeito seja executado após cada
recomposição bem-sucedida. Por outro lado, é incorreto realizar um efeito antes que uma recomposição bem-sucedida seja garantida, que é o caso ao gravar o efeito diretamente em um elemento combinável.
Por exemplo, sua biblioteca de análise pode permitir segmentar a população de usuários anexando metadados personalizados (nesse caso, "propriedades do usuário") a todos os eventos de análise subsequentes. Para comunicar o tipo de usuário atual à biblioteca de análise, use o SideEffect a fim de atualizar o valor da biblioteca.
@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: converter um estado que não é do Compose em um estado do Compose
produceState
inicia uma corrotina com escopo no Compose, que pode enviar valores para um
State retornado. Use essa função
para converter um estado externo em um estado do Compose, por exemplo, para usar um estado externo
por assinatura, como Flow, LiveData ou RxJava, no
Compose.
O produtor é iniciado quando produceState entra na composição e será
cancelado quando ele sair da composição. O State é mesclado.
Definir o mesmo valor não acionará uma recomposição.
Mesmo que o produceState crie uma corrotina, ele também pode ser usado para observar
fontes de dados não suspensas. Para remover a assinatura dessa origem, use
a
função
awaitDispose.
O exemplo a seguir mostra como usar produceState para carregar uma imagem da
rede. A função que pode ser composta loadNetworkImage retorna um State que pode
ser usado em outras funções desse tipo.
@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: converter um ou vários objetos de estado em outro estado
No Compose, a recomposição ocorre sempre que um objeto de estado observado ou uma entrada combinável muda. Um objeto de estado ou entrada pode mudar com mais frequência do que a IU realmente precisa ser atualizada, levando a uma recomposição desnecessária.
Use a derivedStateOf
função quando as entradas de um elemento combinável mudarem com mais frequência do que o necessário
para recompor. Isso geralmente ocorre quando algo está mudando com frequência, como uma posição de rolagem, mas o elemento combinável só precisa reagir a ela quando cruza um determinado limite. derivedStateOf cria um novo objeto de estado do Compose que você pode observar e que só é atualizado conforme necessário. Dessa forma, ele age
de maneira semelhante ao Kotlin Flows
distinctUntilChanged()
operador.
Uso correto
O snippet a seguir mostra um caso de uso adequado para 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() } } }
Nesse snippet, firstVisibleItemIndex muda sempre que o primeiro item visível muda. À medida que você rola, o valor se torna 0, 1, 2, 3, 4, 5 etc. No entanto, a recomposição só precisa ocorrer se o valor for maior que 0.
Essa incompatibilidade na frequência de atualização significa que esse é um bom caso de uso para derivedStateOf.
Uso incorreto
Um erro comum é presumir que, ao combinar dois objetos de estado do Compose, você precisa usar derivedStateOf porque está "derivando o estado". No entanto, isso é puramente uma sobrecarga e não é necessário, conforme mostrado no snippet a seguir:
// 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
Nesse snippet, fullName precisa ser atualizado com a mesma frequência que firstName e lastName. Portanto, não há recomposição excessiva e o uso de derivedStateOf não é necessário.
snapshotFlow: converter o estado do Compose em fluxos
Use snapshotFlow
para converter State<T>
objetos em um fluxo frio. O snapshotFlow executa o próprio bloco quando coletado e emite
o resultado dos objetos State lidos nele. Quando um dos objetos State
lidos no bloco snapshotFlow é modificado, o fluxo emite o novo valor
para o coletor se o novo valor não for igual ao
emitido anteriormente. Esse comportamento é semelhante ao de
Flow.distinctUntilChanged (links em inglês).
O exemplo a seguir mostra um efeito colateral que registra quando o usuário rola pelo primeiro item em uma lista para análise:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it == true } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
No código acima, listState.firstVisibleItemIndex é convertido em um fluxo que
pode se beneficiar dos recursos dos operadores de fluxo.
Como reiniciar efeitos
Alguns efeitos no Compose, como LaunchedEffect, produceState ou
DisposableEffect, recebem um número variável de argumentos e chaves, que são usados
para cancelar o efeito de execução e iniciar um novo argumento com as novas chaves.
A forma típica dessas APIs é:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Devido às particularidades desse comportamento, podem ocorrer problemas se os parâmetros usados para reiniciar o efeito não forem os corretos:
- Ter menos reiniciações de efeitos que o necessário pode causar bugs no app.
- Ter mais reiniciações de efeitos que o necessário pode ser ineficiente.
Como regra geral, as variáveis mutáveis e imutáveis usadas no bloco de
efeito do código precisam ser adicionadas como parâmetros à função do efeito. Além desses parâmetros,
outros podem ser adicionados para que sejam forçados quando o efeito for reiniciado. Se a mudança de
uma variável não fizer com que o efeito seja reiniciado, ela precisará ser envolvida
no rememberUpdatedState. Se a variável nunca mudar porque está unida a um remember sem chaves, você não precisará transmitir a variável como uma chave para o efeito.
No código DisposableEffect mostrado acima, o efeito recebe o lifecycleOwner usado no bloco como um parâmetro, já que qualquer mudança faria com que o efeito fosse reiniciado.
@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) } } }
Os elementos currentOnStart e currentOnStop não são necessários como chaves DisposableEffect,
porque o valor nunca muda na composição devido ao uso do
rememberUpdatedState. Se você não transmitir o lifecycleOwner como um parâmetro e
ele mudar, o elemento HomeScreen será recomposto, mas o DisposableEffect não será descartado
e reiniciado. Essa ação causa problemas porque faz com que o lifecycleOwner incorreto seja
usado desse ponto em diante.
Constantes como chaves
Você pode usar uma constante, como true, como uma chave de efeito para
fazê-la seguir o ciclo de vida do local de chamada. Existem casos de uso válidos dessa
opção, como o exemplo LaunchedEffect mostrado acima. No entanto, pense duas vezes antes de fazer isso e confira se é o que você precisa.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Estado e Jetpack Compose
- Kotlin para Jetpack Compose
- Como usar visualizações no Compose