Um efeito colateral é uma mudança no estado do app que acontece fora do escopo de uma função de composição. 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
: executa funções de suspensão no escopo de um elemento combinável.
Para realizar o trabalho durante a vida útil de um elemento combinável e conseguir chamar
funções suspensas, use o
LaunchedEffect
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, esta é uma animação que pulsa o valor alfa com uma 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 { 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) } }
No código acima, a animação usa a função de suspensão.
delay
aguardar o tempo definido. Depois, ele anima sequencialmente o código
a zero e de volta usando
animateTo
Isso vai ser repetido durante a vida útil do elemento combinável.
rememberCoroutineScope
: receber um escopo com reconhecimento de composição para iniciar uma corrotina fora de um elemento combinável.
Como LaunchedEffect
é uma função que pode ser composta, ela só pode ser usada dentro de
outras funções desse tipo. 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
: publica o estado do Compose em um código que não é dele.
Para compartilhar o estado do Compose com objetos não gerenciados pelo Compose, use o
SideEffect
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 é a
caso ao escrever 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
: converte o estado que não é do Compose para o 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
: converte um ou vários objetos de estado em outro estado.
No Compose, ocorre a recomposição 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 interface realmente precisa ser atualizada, levando a uma recomposição desnecessária.
Use derivedStateOf
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 muda com frequência, como
uma posição de rolagem, mas a função combinável só precisa reagir a ela quando cruza
um certo limite. O derivedStateOf
cria um novo objeto de estado do Compose que você
vai perceber que ele só atualiza o que for necessário. Dessa forma, ele age
semelhante aos fluxos do Kotlin
distinctUntilChanged()
usando um operador lógico.
Uso correto
O snippet a seguir mostra um caso de uso apropriado 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() } } }
Neste snippet, firstVisibleItemIndex
muda sempre que o primeiro item visível
mudanças. Conforme 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 este é um bom caso de uso para
derivedStateOf
:
Uso incorreto
Um erro comum é supor que, ao combinar dois objetos de estado do Compose,
Use derivedStateOf
porque você está "determinando o estado". No entanto,
é puramente sobrecarga e não é necessária, como 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
Neste snippet, fullName
precisa ser atualizado com a mesma frequência que firstName
e
lastName
Portanto, não está ocorrendo nenhuma recomposição excessiva, e usando
derivedStateOf
não é necessário.
snapshotFlow
: converte o estado do Compose em fluxos.
Use snapshotFlow
para converter objetos State<T>
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