Estabilidade no Compose

O Compose considera os tipos como estáveis ou instáveis. Um tipo é estável se é imutável ou quando é 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 pode pular o elemento durante a recomposição (com o modo de pulo forte ativado por padrão a partir do Kotlin 2.0.20):

  • Parâmetros estáveis:o Compose compara parâmetros estáveis usando a 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. Nas 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 aloca com frequência novas instâncias de parâmetros instáveis ou força verificações .equals() caras em grandes coleções ao anotar demais os modelos, você pode 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 de 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. Por isso, 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 mudassem, o Compose não saberia. Isso ocorre porque o Compose só rastreia mudanças nos objetos de estado do Compose.

O Compose considera essa classe instável. Com o modo de pulo forte (Kotlin 2.0.20+), o Compose compara Contact usando a igualdade de instância (===): mudar contact.name no local não aciona a recomposição, enquanto transmitir uma instância Contact recém-alocada com valores idênticos ainda força a recomposição de ContactDetails (e sem o modo de pulo forte, ContactDetails é recomposto sempre que selected muda).

Implementação no Compose

Pode ser útil, mas não é crucial, considerar como o Compose determina quais funções pular durante a recomposição.

Quando o compilador do Compose é executado no seu código, ele marca cada função e tipo com uma das 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. Ele pode marcar uma função como uma, ambas ou nenhuma delas:

  • 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 pulo forte no Kotlin 2.0.20+, todos os elementos que podem ser reiniciados são pulados automaticamente.)
  • Reiniciável: um elemento combinável que pode ser reiniciado serve como um "escopo" em que a recomposição pode começar. Em outras palavras, a função pode ser um ponto de entrada em que o Compose pode começar 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 nunca puder ser alterado 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 com propriedades que podem mudar após a construção. Se e quando essas propriedades mudarem durante o tempo de execução, o Compose vai ficar ciente dessas mudanças.

Depurar a estabilidade

Se o app estiver recompondo um elemento que não teve os parâmetros alterados, primeiro verifique se novas instâncias de tipos instáveis estão sendo alocadas em cada recomposição (ou, se o modo de pulo forte estiver desativado, se o elemento 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 das suas funções de composição para decidir se elas serão comparadas usando a igualdade estrutural (.equals()) ou a igualdade de instância (===) durante a recomposição.
  • Correções imediatas: se você notar que o elemento de composição não está sendo ignorado e isso está causando um problema de desempenho, verifique se novas instâncias de parâmetros instáveis estão sendo recriadas em cada passagem ou se as 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 pulo forte (ativado por padrão no Kotlin 2.0.20+), as coleções instáveis ainda pulam a recomposição usando a igualdade de instância (===) quando a referência da coleção não muda. Tenha cuidado ao substituir coleções padrão por coleções imutáveis do Kotlinx ou ao anotar classes que contêm coleções com @Immutable ou @Stable, já que comparar cada elemento com .equals() em cada passagem de 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 (comparadas usando === no modo de pular forte). Se uma fonte de dados reinstanciar com frequência modelos pequenos e simples de módulos que não são do Compose com valores idênticos, configure um arquivo de configuração de estabilidade ou use @Stable ou @Immutable em que .equals() é barato.

Leitura adicional