Corrigir problemas de estabilidade

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

Ativar ação "pular" com segurança

Primeiro, tente ativar o modo forte de pular. O modo forte para ignorar 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 Pule forte 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 uma 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.
    • Os tipos primitivos, como String, Int e Float, são sempre imutáveis.
    • Se isso for impossível, use o estado do Compose para qualquer propriedade mutável.
  • Stable: indica um tipo mutável. O ambiente de execução do Compose não sabe se e quando qualquer propriedade pública ou comportamento do método do tipo geraria resultados diferentes de uma invocação anterior.

Coleções imutáveis

As coleções são um motivo comum para o Compose considerar uma classe instável. 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, você pode usar coleções imutáveis. O compilador do Compose inclui suporte a Coleções imutáveis 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 haver possíveis mudanças na API.

Considere novamente essa 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, altere o tipo de tags para ImmutableSet<String>:

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

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

Anotar com 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). Você precisa ter muito cuidado com o uso dessas anotações. Substituir o comportamento do compilador pode causar bugs imprevistos, por exemplo, o elemento combinável não é recomposto conforme o esperado.

Se for possível tornar sua classe estável sem uma anotação, tente obter estabilidade dessa maneira.

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

@Immutable
data class Snack(
…
)

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

Classes com anotações em coleções

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

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

Mesmo que você anota 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 das 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 elemento combinável para que seja sempre pulável. Há vários caminhos a seguir.

Há várias maneiras de contornar o problema de coleções instáveis. As subseções a seguir descrevem essas diferentes abordagens.

Arquivo de configuração

Se você quiser cumprir o contrato de estabilidade na sua base de código, poderá optar por considerar as coleções do Kotlin como estáveis adicionando kotlin.collections.* ao seu arquivo de configuração de estabilidade.

Coleção imutável

Para garantir a segurança da imutabilidade no momento de compilação, é possível usar 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, una a 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>
)

Você pode usar isso 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 adotar 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

No Compose Compiler 1.5.5 e versões mais recentes, um arquivo de configuração de classes a serem consideradas estáveis pode ser fornecido durante a compilação. Isso permite considerar como classes que você não controla, como classes de biblioteca padrão, como LocalDateTime, como estáveis.

O arquivo de configuração é de texto simples com uma classe por linha. Comentários, caracteres curinga simples e duplos são aceitos. Confira abaixo um exemplo de configuração:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// 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 as opções do compilador do Compose.

Groovy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Como o compilador do Compose é executado separadamente em cada módulo do projeto, você pode fornecer configurações diferentes para módulos diferentes, se necessário. Como alternativa, defina 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 com vários módulos. O compilador do Compose só poderá inferir se uma classe é estável se todos os tipos não primitivos a que ele faz referência forem 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, esse pode ser um problema.

Solução

Para resolver esse problema, você pode adotar uma das seguintes 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 as classes com @Stable ou @Immutable, quando apropriado.
    • Isso envolve adicionar uma dependência do Compose à camada de dados. No entanto, é 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, caso elas não usem o compilador do Compose.

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

Ao corrigir problemas de estabilidade, não tente tornar todos os elementos puláveis puláveis. Essa tentativa pode levar a uma otimização prematura que apresenta mais problemas do que corrige.

Há muitas situações em que a ação de pular não traz benefícios reais e pode levar à dificuldade de manter o código. Por exemplo:

  • Um elemento combinável que não é recomposto com frequência ou que não é recomposto com frequência.
  • 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 iguais caras. Nesse caso, o custo de verificar se algum parâmetro foi alterado pode superar 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. Também é possível definir que o elemento combinável seja não reiniciável nos casos em que você determina que essa reinicialização representa mais sobrecarga do que vale a pena.