Nesta página, você aprenderá sobre o ciclo de vida de funções que podem ser compostas e como o Compose decide se elas precisam ser recompostas.
Visão geral do ciclo de vida
Como mencionado no artigo Como gerenciar a documentação sobre os estados, uma composição descreve a IU do app e é produzida pela execução de funções que podem ser compostas. Uma composição é uma estrutura em árvore de funções que podem ser compostas que descrevem a IU.
Quando o Jetpack Compose executa suas funções que podem ser compostas pela primeira vez, ele rastreia as funções chamadas durante a composição inicial para descrever a IU em uma composição. Depois, quando o estado do app mudar, o Jetpack Compose programará uma recomposição. A recomposição é quando o Jetpack Compose executa novamente as funções que podem ser compostas que tenham mudado em resposta a mudanças de estado e, em seguida, atualiza a composição para refletir essas mudanças.
Uma composição só pode ser produzida por uma composição inicial e atualizada por recomposição. A única maneira de modificar uma composição é pela recomposição.
Figura 1. Ciclo de vida de uma função que pode ser composta na composição. A função entra na composição, é recomposta zero ou mais vezes e sai da composição.
A recomposição geralmente é acionada por uma mudança em um
objeto State<T>
. O Compose
rastreia esses itens e executa todas as funções que podem ser compostas na Composição que lê
esse State<T>
específico e todas as funções chamadas que não podem ser
ignorados.
Caso uma função que pode ser composta seja chamada várias vezes, diversas instâncias serão colocadas na Composição. Cada chamada tem um ciclo de vida próprio na composição.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Figura 2. Representação de MyComposable
na composição. Se uma
função que pode ser composta for chamada várias vezes, várias instâncias serão colocadas na
composição. Um elemento com uma cor diferente indica
uma instância separada.
Anatomia de uma função que pode ser composta na composição
A instância de uma função que pode ser composta na composição é identificada pelo local de chamada. O compilador Compose considera que cada local de chamada é distinto. Chamar funções de vários locais de chamadas criará diversas instâncias da mesma função na composição.
Se, durante uma recomposição, uma função que pode ser composta chama funções diferentes das chamadas na composição anterior, o Compose identificará quais funções foram chamadas ou não. Para as funções que podem ser compostas chamadas em ambas as composições, o Compose evitará uma recomposição caso as entradas não tenham mudado.
Preservar a identidade é crucial para associar efeitos colaterais a funções que podem ser compostas, para que elas sejam concluídas corretamente, e não reiniciem para cada recomposição.
Veja o exemplo a seguir:
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
No snippet de código acima, LoginScreen
chamará condicionalmente o
LoginError
que pode ser composto e chamará sempre o LoginInput
que pode ser composto. Cada
chamada tem um local e posição de origem exclusivos, que o compilador usará para
identificá-la.
Figura 3. Representação de LoginScreen
na composição quando o estado
muda e uma recomposição ocorre. A mesma cor indica que não houve recomposição.
Embora LoginInput
tenha passado de primeiro a ser chamado para segundo,
a instância LoginInput
será preservada após as recomposições. Além disso,
como LoginInput
não tem parâmetros que mudaram na
recomposição, a chamada para LoginInput
será ignorada pelo Compose.
Adicionar mais informações para ajudar na recomposição inteligente
Chamar uma função que pode ser composta várias vezes fará com que ela também seja adicionada várias vezes à composição. Ao chamar uma função do mesmo local de chamadas várias vezes, o Compose não recebe informações para identificar cada chamada de forma exclusiva. Assim, a ordem de execução é usada junto ao local da chamada para diferenciar as instâncias. Algumas vezes, esse comportamento é o suficiente. Mas, em alguns casos, ele pode causar um comportamento indesejado.
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
No exemplo acima, o Compose usa a ordem de execução, além do local da chamada
para diferenciar as instâncias na composição. Se um novo movie
for adicionado
ao fim da lista, o Compose poderá reutilizar as instâncias que já estão na composição, já que as posições na lista não mudaram e, portanto, a entrada movie
é a mesma para essas instâncias.
Figura 4. Representação de MoviesScreen
na composição quando um novo
elemento é adicionado ao fim da lista. Os MovieOverview
que podem ser compostos na
composição podem ser reutilizados. A mesma cor em MovieOverview
indica que não ocorreu a recomposição
da função.
No entanto, se a lista movies
mudar adicionando itens ao topo ou ao
meio, removendo ou reorganizando itens, ela causará uma recomposição
em todas as chamadas MovieOverview
cujo parâmetro de entrada mudou de posição na
lista. Isso é extremamente importante se, por exemplo, MovieOverview
buscar uma
imagem de filmes usando um efeito colateral. Se a reposição ocorrer enquanto o efeito estiver em
andamento, ela será cancelada e começará novamente.
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
Figura 5. Representação de MoviesScreen
na composição quando um
novo elemento é adicionado à lista. Os elementos MovieOverview
que podem ser compostos não podem ser reutilizados e
todos os efeitos colaterais são reiniciados. Uma cor diferente em MovieOverview
indica que a função foi recomposta.
O ideal é que a identidade da instância MovieOverview
seja considerada como
vinculada à identidade do movie
que é passada a ela. Se a lista de filmes
for reordenada, o ideal será reorganizar as instâncias correspondentes na
árvore de composição, em vez de recompor cada função MovieOverview
em
outra instância de filme. O Compose oferece uma maneira de informar ao ambiente de execução
quais valores você quer usar para identificar uma determinada parte da árvore: a
key
que pode ser composta.
Ao envolver um bloco de código usando uma chamada à chave que pode ser composta com um ou mais
valores transmitidos, esses valores serão combinados para identificar essa
instância na composição. O valor de uma key
não precisa ser
globalmente exclusivo. Ele precisa ser único apenas entre as invocações de
funções no local de chamada. Portanto, nesse exemplo, cada movie
precisa ter uma
key
que seja única entre os movies
. Não há problema se essa for a mesma key
de outra função em outro local do app.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
Com o exemplo acima, mesmo que os elementos na lista mudem, o Compose reconhece
chamadas individuais para MovieOverview
e pode reutilizá-las.
Figura 6. Representação de MoviesScreen
na composição quando um
novo elemento é adicionado à lista. Como as funções MovieOverview
têm chaves exclusivas,
o Compose reconhece quais instâncias de MovieOverview
não mudaram e
podem reutilizá-las. Os efeitos colaterais continuarão sendo executados.
Algumas funções têm compatibilidade integrada com a key
. Por exemplo,
LazyColumn
aceita especificar uma key
personalizada na DSL items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Como ignorar caso as entradas não tenham mudado
Durante a recomposição, a execução de algumas funções combináveis qualificadas pode ser ignorada completamente se as entradas não tiverem mudado em relação à composição anterior.
Uma função combinável pode ser ignorada a menos que:
- A função tem um tipo de retorno que não é
Unit
- A função é anotada com
@NonRestartableComposable
ou@NonSkippableComposable
. - Um parâmetro obrigatório é de um tipo não estável
Há um modo experimental do compilador, Strong Skipping, que relaxa o último requisito.
Para que um tipo seja considerado estável, ele precisa obedecer ao seguinte contrato:
- O resultado de
equals
para duas instâncias sempre será o mesmo para essas duas instâncias. - Se uma propriedade pública do tipo mudar, a composição será notificada.
- Todos os tipos de propriedade pública também estão estáveis.
Há alguns tipos comuns importantes que se enquadram nesses termos que serão tratados como estáveis pelo
compilador do Compose, mesmo que não estejam explicitamente
marcados como estáveis pela anotação @Stable
:
- Todos os tipos de valor primitivo:
Boolean
,Int
,Long
,Float
,Char
etc. - Strings
- Todos os tipos de função (lambdas)
Todos esses tipos podem seguir os termos de estabilidade porque são imutáveis. Como os tipos imutáveis nunca mudam, eles nunca precisam notificar a composição sobre uma mudança. Por esse motivo, fica muito mais fácil seguir os termos.
Um tipo importante que é estável, mas é mutável é o tipo MutableState
do
Compose. Se um valor for mantido em um MutableState
, o objeto de estado será
considerado estável de forma geral, porque o Compose será notificado sobre qualquer mudança na
propriedade .value
do State
.
Quando todos os tipos passados como parâmetros para uma função são estáveis, os valores são comparados para verificar se eles são iguais, com base na posição da função na árvore de IU. A recomposição será ignorada caso todos os valores tenham permanecido inalterados desde a chamada anterior.
O Compose só considera que um tipo é estável se for possível provar isso. Por exemplo, uma interface geralmente é tratada como não estável, assim como tipos com propriedades públicas mutáveis (cuja implementação pode ser imutável).
Caso o Compose não consiga concluir que um tipo é estável, mas você queira forçá-lo a
tratar o tipo como estável, marque-o com a
anotação @Stable
.
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
No snippet de código acima, como UiState
é uma interface, geralmente o Compose poderia
considerar que esse tipo não é estável. Ao adicionar a anotação @Stable
,
você informa ao Compose que esse tipo é estável, permitindo que ele priorize
recomposições inteligentes. Isso também significa que o Compose tratará todas as
implementações da interface como estáveis se ela for usada como o tipo de parâmetro.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Estado e Jetpack Compose
- Efeitos colaterais no Compose
- Salvar o estado da interface no Compose