Corrigir problemas de estabilidade

Quando você se deparar com uma classe instável que causa problemas de desempenho, é preciso estabilizá-la. Este documento descreve várias técnicas que você pode usar para fazer isso.

Ativar a rejeição avançada

Primeiro, tente ativar o modo de rejeição avançada. O modo de rejeição avançada permite rejeitar elementos combináveis com parâmetros instáveis e é o método mais fácil para corrigir problemas de desempenho causados pela estabilidade.

Consulte Ignorar com força para mais informações.

Tornar a classe imutável

Você também pode tentar tornar uma classe instável completamente imutável.

  • Imutável: indica um tipo em que o valor de qualquer propriedade nunca muda depois que uma instância desse tipo é construída, e todos os métodos são referencialmente transparentes.
    • Verifique se todas as propriedades da classe são val em vez de var e de tipos imutáveis.
    • Tipos primitivos, como String, Int e Float, são sempre imutáveis.
    • Se isso não for possível, use o estado do Compose para qualquer propriedade mutável.
  • Estável: indica um tipo mutável. O tempo de execução do Compose não fica sabendo se e quando alguma das propriedades públicas ou do comportamento do método do tipo geraria resultados diferentes de uma invocação anterior.

Coleções imutáveis

Um motivo comum para o Compose considerar uma classe instável são as coleções. Conforme observado na página Diagnosticar problemas de estabilidade, o compilador do Compose não pode ter certeza absoluta de que coleções como List, Map e Set são realmente imutáveis e, portanto, as marca como instáveis.

Para resolver isso, use coleções imutáveis. O compilador do Compose inclui suporte para Kotlinx Immutable Collections. Essas coleções são garantidas como imutáveis, e o compilador do Compose as trata como tal. Essa biblioteca ainda está em versão Alfa. Portanto, é possível que a API dela mude.

Considere novamente esta classe instável do guia Diagnosticar problemas de estabilidade:

unstable class Snack {
  
  unstable val tags: Set<String>
  
}

É possível tornar tags estável usando uma coleção imutável. Na classe, mude o tipo de tags para ImmutableSet<String>:

data class Snack{
    
    val tags: ImmutableSet<String> = persistentSetOf()
    
}

Depois disso, todos os parâmetros da classe são imutáveis, e o compilador do Compose marca a classe como estável.

Adicione as anotações Stable ou Immutable.

Uma possível maneira de resolver problemas de estabilidade é anotar classes instáveis com @Stable ou @Immutable.

A anotação de uma classe substitui o que o compilador inferiria sobre ela. É semelhante ao operador !! em Kotlin. Tenha muito cuidado ao usar essas anotações. Substituir o comportamento do compilador pode levar a bugs imprevistos, como a não recomposição do elemento combinável quando você espera que isso aconteça.

Se for possível tornar sua classe estável sem uma anotação, tente alcançar a estabilidade dessa forma.

O snippet a seguir mostra um exemplo mínimo de uma classe de dados com a anotação imutável:

@Immutable
data class Snack(

)

Se você usar a anotação @Immutable ou @Stable, o compilador do Compose marcará a classe Snack como estável.

Classes anotadas em coleções

Considere um elemento combinável que inclui um parâmetro do tipo List<Snack>:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  
  unstable snacks: List<Snack>
  
)

Mesmo que você anote Snack com @Immutable, o compilador do Compose ainda vai marcar o parâmetro snacks em HighlightedSnacks como instável.

Os parâmetros enfrentam o mesmo problema que as classes quando se trata de tipos de coleção. O compilador do Compose sempre marca um parâmetro do tipo List como instável, mesmo quando é uma coleção de tipos estáveis.

Não é possível marcar um parâmetro individual como estável nem anotar um combinável para que ele sempre possa ser ignorado. Há vários caminhos para seguir.

Há várias maneiras de contornar o problema das coleções instáveis. As subseções abaixo apresentam essas diferentes abordagens.

Arquivo de configuração

Se você quiser obedecer ao contrato de estabilidade na sua base de código, poderá ativar a consideração das coleções Kotlin como estáveis adicionando kotlin.collections.* ao arquivo de configuração de estabilidade.

Coleção imutável

Para ter segurança de imutabilidade no tempo de compilação, use uma coleção imutável do kotlinx em vez de List.

@Composable
private fun HighlightedSnacks(
    
    snacks: ImmutableList<Snack>,
    
)

Wrapper

Se não for possível usar uma coleção imutável, crie a sua. Para fazer isso, encapsule o List em uma classe estável anotada. Um wrapper genérico provavelmente é a melhor escolha para isso, dependendo dos seus requisitos.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

Em seguida, use isso como o tipo do parâmetro no seu elemento combinável.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

Solução

Depois de adotar uma dessas abordagens, o compilador do Compose vai marcar o elemento combinável HighlightedSnacks como skippable e restartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Durante a recomposição, o Compose agora pode ignorar HighlightedSnacks se nenhuma das entradas tiver mudado.

Arquivo de configuração de estabilidade

A partir do Compose Compiler 1.5.5, é possível fornecer um arquivo de configuração de classes a serem consideradas estáveis no tempo de compilação. Isso permite considerar como estáveis classes que você não controla, como as da biblioteca padrão, como LocalDateTime.

O arquivo de configuração é um arquivo de texto simples com uma classe por linha. Comentários, curingas simples e duplos são aceitos.

Exemplo de configuração:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Para ativar esse recurso, transmita o caminho do arquivo de configuração para o bloco de opções composeCompiler da configuração do plug-in do Gradle do compilador do Compose.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

Como o compilador do Compose é executado em cada módulo do projeto separadamente, você pode fornecer configurações diferentes para módulos diferentes, se necessário. Outra opção é ter uma configuração no nível raiz do projeto e transmitir esse caminho para cada módulo.

Vários módulos

Outro problema comum envolve a arquitetura de vários módulos. O compilador do Compose só pode inferir se uma classe é estável se todos os tipos não primitivos que ela referencia forem explicitamente marcados como estáveis ou estiverem em um módulo que também foi criado com o compilador do Compose.

Se a camada de dados estiver em um módulo separado da camada de UI, que é a abordagem recomendada, esse pode ser um problema que você vai encontrar.

Solução

Para resolver esse problema, siga uma destas abordagens:

  1. Adicione as classes ao arquivo de configuração do compilador.
  2. Ative o compilador do Compose nos módulos da camada de dados ou marque suas classes com @Stable ou @Immutable quando apropriado.
    • Isso envolve adicionar uma dependência do Compose à sua camada de dados. No entanto, ela é apenas a dependência do tempo de execução do Compose e não do Compose-UI.
  3. No módulo de UI, encapsule as classes da camada de dados em classes wrapper específicas da UI.

O mesmo problema também ocorre ao usar bibliotecas externas se elas não usarem o compilador do Compose.

Nem todo elemento combinável precisa ser ignorável

Ao trabalhar para corrigir problemas de estabilidade, não tente tornar todos os combináveis ignoráveis. Tentar fazer isso pode levar a uma otimização prematura que introduz mais problemas do que corrige.

Há muitas situações em que ser ignorável não tem nenhum benefício real e pode levar a um código difícil de manter. Exemplo:

  • Um elemento combinável que não é recomposto com frequência ou nunca.
  • Um elemento combinável que chama apenas elementos combináveis que podem ser ignorados.
  • Um elemento combinável com um grande número de parâmetros e implementações de igualdade caras. Nesse caso, o custo de verificar se algum parâmetro mudou pode ser maior do que o de uma recomposição barata.

Quando um elemento combinável pode ser ignorado, ele adiciona uma pequena sobrecarga que pode não valer a pena. Você pode até mesmo anotar seu elemento combinável como não reinicializável em casos em que você determina que ser reinicializável é mais sobrecarga do que vale a pena.