Corrigir problemas de estabilidade

Quando você tiver uma classe instável que causa problemas de desempenho, precisará torná-la estável. Este documento descreve várias técnicas que podem ser usadas para fazer isso.

Ativar a rejeição avançada

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

Consulte Pular com segurança para mais informações.

Tornar a classe imutável

Também é possível tentar tornar uma classe instável completamente imutável.

  • Imutável: indica um tipo em que o valor de qualquer propriedade nunca pode mudar 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 são de tipos imutáveis.
    • Tipos primitivos, como String, Int e Float, são sempre imutáveis.
    • Se isso for impossível, use o estado do Compose para todas as propriedades mutáveis.
  • Estável: indica um tipo mutável. O ambiente de execução do Compose não reconhece se e quando o comportamento de métodos ou propriedades públicas do tipo vai gerar resultados diferentes de uma invocação anterior.

Coleções imutáveis

As coleções são um motivo comum pelo qual o Compose considera uma classe instável. Conforme observado na página Diagnosticar problemas de estabilidade, o compilador do Compose não pode ter certeza 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 a coleções imutáveis do Kotlinx. Essas coleções são imutáveis, e o compilador do Compose as trata dessa forma. Essa biblioteca ainda está na versão Alfa, então podem ocorrer mudanças na API dela.

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.

Um caminho possível para 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. Ele é semelhante ao operador !! no Kotlin (link em inglês). É preciso ter muito cuidado com o uso dessas anotações. Modificar o comportamento do compilador pode levar a bugs imprevistos, como a recomposição do elemento combinável conforme esperado.

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

O snippet a seguir fornece um exemplo mínimo de uma classe de dados anotada como imutável:

@Immutable
data class Snack(
…
)

Se você usa a anotação @Immutable ou @Stable, o compilador do Compose marca 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ê anexe Snack com @Immutable, o compilador do Compose ainda marca 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 se trata de 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 seja sempre ignorável. Há vários caminhos a seguir.

Há várias maneiras de contornar o problema de 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, pode considerar as coleções do Kotlin como estáveis adicionando kotlin.collections.* ao arquivo de configuração de estabilidade.

Coleção imutável

Para 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, você poderá criar a própria. Para fazer isso, junte o List em uma classe estável com anotação. Um wrapper genérico é provavelmente a melhor escolha para isso, dependendo dos seus requisitos.

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

Em seguida, ele pode ser usado como o tipo do parâmetro no elemento combinável.

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

Solução

Depois de usar qualquer uma dessas abordagens, o compilador do Compose agora marca 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 pular HighlightedSnacks se nenhuma das entradas tiver mudado.

Arquivo de configuração de estabilidade

A partir do Compose Compiler 1.5.5, um arquivo de configuração de classes a serem considerados estáveis pode ser fornecido durante a compilação. Isso permite considerar classes que você não controla, como classes de biblioteca padrão, como LocalDateTime, como estáveis.

O arquivo de configuração é um arquivo de texto simples com uma classe por linha. Comentários e caracteres curinga simples e duplos são aceitos. Um 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 para 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. Como alternativa, tenha uma configuração no nível raiz do projeto e transmita 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 a que ela faz referência estiverem explicitamente marcados como estáveis ou 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 interface, que é a abordagem recomendada, talvez você encontre esse tipo de problema.

Solução

Para resolver esse problema, siga uma destas abordagens:

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

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

Nem todos os elementos combináveis devem ser puláveis

Ao corrigir problemas de estabilidade, não tente pular todos os elementos combináveis. Tentar fazer isso pode levar a uma otimização prematura que apresenta mais problemas do que correções.

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

  • Um elemento combinável que não é recomposto com frequência ou que não é recomposto.
  • Um elemento combinável que, por si só, chama elementos combináveis puláveis.
  • Um elemento combinável com um grande número de parâmetros com implementações igualmente caras. Nesse caso, o custo de verificar se algum parâmetro foi alterado pode ser maior do que o custo de uma recomposição barata.

Quando um elemento combinável é pulável, ele adiciona uma pequena sobrecarga que pode não valer a pena. Você pode até mesmo anotar o elemento combinável para que ele não seja reiniciável nos casos em que você determinar que ser reiniciável gera mais sobrecarga do que vale a pena.