Estabilidade no Compose

O Compose considera os tipos como estáveis ou instáveis. Um tipo é estável se ele for imutável ou se for possível para o Compose saber se o valor dele mudou entre as recomposições. Um tipo é instável se o Compose não consegue saber se o valor dele mudou entre as recomposições.

O Compose usa a estabilidade dos parâmetros de um elemento combinável para determinar como comparar entradas e decidir se é possível pular o elemento combinável durante a recomposição (com o modo de rejeição forte ativado por padrão a partir do Kotlin 2.0.20):

  • Parâmetros estáveis:o Compose compara parâmetros estáveis usando igualdade estrutural (Object.equals()). Se os parâmetros estáveis de um elemento combinável forem iguais aos valores anteriores, o Compose vai ignorá-lo.
  • Parâmetros instáveis:com o modo de rejeição avançada ativado (o padrão no Kotlin 2.0.20 e versões mais recentes), o Compose compara parâmetros instáveis usando a igualdade de instância (===) e pula o elemento combinável se as mesmas instâncias de objeto forem transmitidas. Em versões anteriores ao Kotlin 2.0.20 (ou quando o modo de rejeição avançada está desativado), o Compose sempre recompõe um elemento combinável com parâmetros instáveis quando o elemento pai é recomposto.

Se o app alocar com frequência novas instâncias de parâmetros instáveis ou forçar verificações .equals() caras em grandes coleções ao anotar demais os modelos, você poderá observar recomposições desnecessárias ou sobrecarga de comparação.

Este documento detalha como o Compose determina a estabilidade e como você pode otimizá-la para melhorar o desempenho e a experiência geral do usuário.

Objetos imutáveis

Os snippets a seguir demonstram os princípios gerais por trás da estabilidade e da recomposição.

A classe Contact é uma classe de dados imutável. Isso ocorre porque todos os parâmetros são primitivos definidos com a palavra-chave val. Depois de criar uma instância de Contact, não é possível mudar o valor das propriedades do objeto. Se você tentasse fazer isso, criaria um novo objeto.

data class Contact(val name: String, val number: String)

O elemento combinável ContactRow tem um parâmetro do tipo Contact.

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }

   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

Considere o que acontece quando o usuário clica no botão ativar/desativar e o estado selected muda:

  1. O Compose avalia se precisa recompor o código dentro de ContactRow.
  2. Ele vê que o único argumento para ContactDetails é do tipo Contact.
  3. Como Contact é uma classe de dados imutável, o Compose tem certeza de que nenhum dos argumentos de ContactDetails mudou.
  4. Assim, o Compose pula ContactDetails e não faz a recomposição.
  5. Por outro lado, os argumentos de ToggleButton mudaram, e o Compose recompõe esse componente.

Objetos mutáveis

Embora o exemplo anterior use um objeto imutável, é possível criar um objeto mutável. Considere o seguinte snippet:

data class Contact(var name: String, var number: String)

Como cada parâmetro de Contact agora é um var, a classe não é mais imutável. Se as propriedades dele mudassem, o Compose não ficaria sabendo. Isso ocorre porque o Compose só monitora mudanças nos objetos de estado do Compose.

O Compose considera essa classe instável. Com o modo de remoção forte (Kotlin 2.0.20 ou mais recente), o Compose compara Contact usando a igualdade de instâncias (===): mudar contact.name no lugar não vai acionar a recomposição, enquanto passar uma instância Contact recém-alocada com valores idênticos ainda vai forçar ContactDetails a recompor (e sem o modo de remoção forte, ContactDetails é recomposto sempre que selected muda).

Implementação no Compose

É útil, mas não essencial, considerar como o Compose determina quais funções ignorar durante a recomposição.

Quando o compilador do Compose é executado no seu código, ele marca cada função e tipo com uma de várias tags. Essas tags refletem como o Compose processa a função ou o tipo durante a recomposição.

Funções

O Compose pode marcar funções como skippable ou restartable. Ela pode marcar uma função como uma, ambas ou nenhuma destas opções:

  • Ignorável: se o compilador marcar um elemento combinável como ignorável, o Compose poderá ignorá-lo durante a recomposição se todos os argumentos forem iguais aos valores anteriores. Com o modo de rejeição avançada no Kotlin 2.0.20 ou mais recente, todos os combináveis reinicializáveis podem ser rejeitados automaticamente.
  • Reinicializável: um elemento combinável reinicializável serve como um "escopo" em que a recomposição pode começar. Em outras palavras, a função pode ser um ponto de entrada para que o Compose comece a executar novamente o código para recomposição após mudanças de estado.

Tipos

O Compose marca os tipos como imutáveis ou estáveis. Cada tipo é um ou outro:

  • Imutável: o Compose marca um tipo como imutável se o valor das propriedades dele nunca puder mudar e todos os métodos forem referencialmente transparentes.
    • Todos os tipos primitivos são marcados como imutáveis. Isso inclui String, Int e Float.
  • Estável: indica um tipo cujas propriedades podem mudar após a construção. Se e quando essas propriedades mudarem durante o tempo de execução, o Compose vai detectar essas mudanças.

Estabilidade de depuração

Se o app estiver recompondo um elemento combinável cujos parâmetros não mudaram, primeiro verifique se novas instâncias de tipos instáveis estão sendo alocadas em cada recomposição ou, se o modo de rejeição forte estiver desativado, se o elemento combinável tem parâmetros com propriedades var ou val de um tipo instável.

Para informações detalhadas sobre como diagnosticar problemas complexos de estabilidade no Compose, consulte o guia Depurar estabilidade.

Corrigir problemas de estabilidade

Para saber como trazer estabilidade à sua implementação do Compose, consulte o guia Corrigir problemas de estabilidade.

Resumo

Em geral, observe os seguintes pontos:

  • Parâmetros: o Compose determina a estabilidade de cada parâmetro dos combináveis para decidir se eles serão comparados usando igualdade estrutural (.equals()) ou de instância (===) durante a recomposição.
  • Correções imediatas: se você notar que o elemento combinável não está sendo ignorado e está causando um problema de desempenho, verifique se novas instâncias de parâmetros instáveis estão sendo recriadas em cada transmissão ou se propriedades var estão sendo usadas em vez de State.
  • Relatórios do compilador: use os relatórios do compilador para determinar qual estabilidade está sendo inferida sobre suas classes.
  • Coleções: o Compose considera as interfaces de coleção padrão (List, Set e Map) instáveis porque as implementações subjacentes podem ser mutáveis. Com o modo de remoção forte (ativado por padrão no Kotlin 2.0.20+), as coleções instáveis ainda ignoram a recomposição usando a igualdade rápida de instâncias O(1) (===) quando a referência da coleção não muda. Evite converter coleções grandes em coleções imutáveis do Kotlinx ou anotar classes que contêm coleções com @Immutable ou @Stable, a menos que a igualdade estrutural (O(N) .equals()) seja especificamente necessária, já que comparar todos os elementos em cada recomposição pode ser mais caro do que a própria recomposição.
  • Outros módulos: o Compose considera instáveis as classes de módulos em que o compilador do Compose não é executado (comparação usando === no modo de omissão forte). Se uma fonte de dados recriar com frequência modelos pequenos e simples de módulos não relacionados ao Compose com valores idênticos, configure um arquivo de configuração de estabilidade ou use @Stable ou @Immutable em que .equals() é barato.

Leitura adicional