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: executar funções de suspensão no escopo de uma função que pode ser composta
Para chamar funções de suspensão de forma segura em uma função de composição, use o
LaunchedEffect
de composição. 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, a ação de mostrar uma Snackbar
em um Scaffold
é realizada pela função SnackbarHostState.showSnackbar
, que é uma função de suspensão.
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// `state.hasError` is false, and only start when `state.hasError` is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
No código acima, uma corrotina será acionada caso o estado contenha um erro e
será cancelada quando isso não ocorrer. Como o local de chamadas LaunchedEffect
está dentro
de uma instrução if, quando a instrução for falsa, se LaunchedEffect
estiver na composição,
ele será removido e, portanto, a corrotina será cancelada.
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 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(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
scaffoldState.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
que pode ser composto, que é invocado a cada recomposição concluída corretamente.
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 rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// 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
}
productState: 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
): 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
Use
derivedStateOf
para calcular ou derivar um determinado estado de outros objetos de estado. O
uso dessa função garante que o cálculo só ocorra quando um
dos estados usados no cálculo mudar.
O exemplo a seguir mostra uma lista de Tarefas básica, em que as tarefas com palavras-chave de alta prioridade definidas pelo usuário aparecem primeiro:
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
// Calculate high priority tasks only when the todoTasks or highPriorityKeywords
// change, not on every recomposition
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
/* Rest of the UI where users can add elements to the list */
}
}
No código acima, a função derivedStateOf
garante que sempre que todoTasks
mudar, o cálculo de highPriorityTasks
será executado
e a IU será atualizada com o resultado. Se highPriorityKeywords
mudar, o
bloco remember
será executado e um novo objeto de estado derivado será criado
e lembrado no lugar do antigo. Como o filtro para calcular
highPriorityTasks
pode ser caro, ele só precisa ser executado quando uma das
listas mudar, e não a cada recomposição.
Além disso, uma atualização no estado gerada pela função derivedStateOf
não faz
com que o elemento em que ela foi declarado seja recomposto.
O Compose só recompõe elementos onde o estado retornado é lido, dentro de LazyColumn
no
exemplo.
O código também presume que highPriorityKeywords
muda com uma frequência
menor do que todoTasks
. Se esse não for o caso, o código pode usar
remember(todoTasks, highPriorityKeywords)
em vez da derivedStateOf
.
snapshotFlow: converter 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.