Talvez você encontre armadilhas comuns do Compose. Esses erros podem gerar um código que parece funcionar bem o suficiente, mas pode prejudicar o desempenho da interface. Siga as práticas recomendadas para otimizar seu app no Compose.
Usar remember
para minimizar os cálculos caros
As funções combináveis podem ser executadas com muita frequência, como para cada frame de uma animação. Por isso, faça o menor cálculo possível no corpo da função.
Uma técnica importante é armazenar os resultados dos cálculos com
remember
. Dessa forma, o cálculo é executado uma vez e você pode buscar os
resultados sempre que eles forem necessários.
Por exemplo, confira este código que mostra uma lista ordenada de nomes, mas que faz a classificação de maneira muito cara:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
Toda vez que a ContactsList
é recomposta, toda a lista de contatos é classificada
novamente, mesmo que ela não tenha mudado. Se o usuário rolar a lista,
o elemento combinável vai ser recomposto sempre que uma nova linha aparecer.
Para resolver esse problema, classifique a lista fora de LazyColumn
e armazene a
lista ordenada com remember
:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
Agora, a lista é classificada uma vez, quando a ContactList
é composta pela primeira vez. Se os
contatos ou o comparador mudarem, a lista classificada vai ser gerada novamente. Caso contrário, o
elemento de composição pode continuar usando a lista classificada em cache.
Usar chaves de layout lentas
Os layouts lentos reutilizam itens de forma eficiente, gerando ou recompostos apenas quando necessário. No entanto, você pode ajudar a otimizar layouts lentos para recomposição.
Suponha que uma operação do usuário faça com que um item seja movido na lista. Por exemplo, suponha que você mostre uma lista de notas classificadas por hora de modificação com a nota modificada mais recentemente no topo.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
Mas há um problema com este código. Suponha que a última nota seja alterada. Ela passa a ser a nota modificada mais recentemente e, portanto, ela vai para o topo da lista, e todas as outras descem uma posição.
Sem sua ajuda, o Compose não percebe que os itens que não mudaram estão apenas sendo movidos na lista. Em vez disso, o Compose acha que o antigo "item 2" foi excluído e um novo foi criado para o item 3, item 4 e todo o restante. O resultado é que o Compose faz a recomposição de todos os itens na lista, mesmo que apenas um deles tenha mudado.
A solução é fornecer chaves de item. O fornecimento de uma chave estável para cada item permite que o Compose evite recomposições desnecessárias. Nesse caso, o Compose pode determinar que o item que estava na posição 3 agora é o mesmo que estava na posição 2. Como nenhum dos dados desse item foi modificado, o Compose não precisa fazer a recomposição.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
Usar derivedStateOf
para limitar as recomposições
Um risco de usar o estado nas composições é que, se ele muda rapidamente, a IU pode ser recomposta mais do que o necessário. Por exemplo, suponha que você está mostrando uma lista rolável. Examine o estado da lista para verificar qual é o primeiro item visível:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
O problema é que, se o usuário rola a lista, o listState
muda constantemente,
conforme o usuário arrasta o dedo. Isso significa que a lista está sendo
recomposta constantemente. No entanto, você não precisa fazer a recomposição com essa frequência. Isso
não é necessário até que um novo item se torne visível na parte de baixo. Isso exige muitos cálculos desnecessários, o que faz com que a IU tenha uma performance ruim.
A solução é usar o estado derivado. O estado derivado permite informar ao Compose quais mudanças de estado realmente acionam a recomposição. Nesse caso, especifique quando o primeiro item visível muda. Quando esse valor de estado muda, a interface precisa ser recomposta, mas se o usuário ainda não rolar o suficiente para levar um novo item ao topo, não é necessário recompor.
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
Adiar leituras pelo maior tempo possível
Quando um problema de desempenho é identificado, o adiamento de leituras de estado pode ajudar. Adiar leituras de estado garante que o Compose execute novamente o mínimo possível de código na recomposição. Por exemplo, se a IU tiver um estado elevado no lugar da árvore combinável e você ler o estado em um elemento combinável filho, vai ser possível unir o estado de leitura em uma função lambda. Isso faz com que a leitura ocorra somente quando necessário. Para referência, consulte a implementação no app de exemplo Jetsnack (link em inglês). O Jetsnack implementa um efeito semelhante a uma barra de ferramentas de recolhimento na tela de detalhes. Para entender por que essa técnica funciona, consulte a postagem do blog Jetpack Compose: recomposição de depuração.
Para conseguir esse efeito, o elemento combinável Title
precisa do deslocamento de rolagem
para se deslocar usando um Modifier
. Veja uma versão simplificada do
código do Jetsnack antes que a otimização seja feita:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
Quando o estado de rolagem muda, o Compose invalida o escopo de recomposição
pai mais próximo. Nesse caso, o escopo mais próximo é o elemento combinável SnackDetail
. Observe que Box
é uma função inline e, portanto, não é um escopo de
recomposição. O Compose faz a recomposição da SnackDetail
e de todos os elementos combináveis dentro de
SnackDetail
. Se você mudar o código para ler apenas o estado em que ele é
realmente usado, é possível reduzir o número de elementos que precisam ser recompostos.
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
O parâmetro de rolagem agora é uma lambda. Isso significa que o Title
ainda pode fazer referência ao
estado elevado, mas o valor só é lido dentro de Title
, onde é realmente
necessário. Como resultado, quando o valor de rolagem mudar, o escopo de recomposição
mais próximo vai ser o elemento de composição Title
. O Compose não precisa mais recompor
toda a Box
.
Essa é uma boa melhoria, mas você pode fazer ainda melhor. Verifique se
você não está fazendo a recomposição apenas para mudar o layout ou redesenhar um elemento de composição. Neste
caso, você só está mudando o deslocamento do elemento de composição Title
,
o que pode ser feito na fase de layout.
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
Anteriormente, o código usava Modifier.offset(x: Dp, y: Dp)
, que usa o
deslocamento como um parâmetro. Ao mudar para a versão lambda do modificador,
você pode garantir que a função leia o estado de rolagem na fase de layout. Como
resultado, quando o estado de rolagem muda, o Compose pode pular completamente a fase de composição
e ir diretamente para a fase de layout. Ao transmitir com frequência
mudanças de variáveis de estado em modificadores, use as versões lambda dos
modificadores sempre que possível.
Confira outro exemplo dessa abordagem. Este código ainda não foi otimizado:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
Aqui, a cor de fundo da caixa alterna rapidamente entre duas cores. Esse estado muda com muita frequência. A função de composição lê esse estado no modificador em segundo plano. Como resultado, a caixa precisa ser recomposta em cada frame, já que a cor muda em cada um.
Para melhorar isso, use um modificador baseado em lambda, neste caso, drawBehind
.
Isso significa que o estado da cor só é lido durante a fase de exibição. Como resultado,
o Compose pode pular completamente as fases de composição e layout. Quando a cor
muda, ele vai direto para a fase de exibição.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
Evitar gravações inversas
O Compose tem como base a suposição que você nunca vai gravar em um estado que já foi lido. Isso é conhecido como gravação inversa e pode fazer com que a recomposição ocorra em cada frame, indefinidamente.
O elemento combinável abaixo mostra um exemplo desse tipo de erro.
@Composable fun BadComposable() { var count by remember { mutableStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
Esse código atualiza a contagem no final do elemento combinável depois de lê-lo na linha anterior. Se você executar esse código, vai notar que, depois de clicar no botão, o que causa uma recomposição, o contador aumenta rapidamente em um loop infinito, conforme o Compose recompõe o elemento combinável, detecta uma leitura de estado desatualizada e agenda outra recomposição.
Nunca gravar no estado da composição evita gravações
inversas. Se possível, grave sempre no estado em resposta a um evento
e em uma lambda, como no exemplo onClick
anterior.
Outros recursos
- Guia de desempenho do app: conheça práticas recomendadas, bibliotecas e ferramentas para melhorar o desempenho no Android.
- Inspecionar o desempenho:inspecione o desempenho do app.
- Comparativo de mercado:comparativo de mercado do desempenho do app.
- Inicialização do app:otimiza a inicialização do app.
- Perfis de referência:entenda os perfis de referência.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Estado e Jetpack Compose
- Modificadores gráficos
- Como trabalhar com o Compose