Corrigir problemas de estabilidade

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

Tornar a classe imutável

Primeiro, tente 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 é criada, 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.
    • Os 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 sabe se e quando qualquer propriedade pública ou comportamento do método do tipo gera resultados diferentes de uma invocação anterior.

Coleções imutáveis

Um motivo comum pelo qual o Compose considera uma classe instável são coleções. 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 esse problema, use 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 como tal. Essa biblioteca ainda está na versão Alfa, então podem ocorrer 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 o 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 marcará a classe 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.

Anotar uma classe significa substituir o que o compilador inferiria sobre ela. Ele é semelhante ao !! operador em Kotlin. Tenha muito cuidado sobre o uso dessas anotações. Modificar o comportamento do compilador pode levar a bugs imprevistos, por exemplo, o elemento combinável não pode ser recomposto como esperado.

Se for possível tornar sua classe estável sem uma anotação, tente alcançar a 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 se você anotar Snack com @Immutable, o compilador do Compose ainda 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 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 para a frente.

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 respeitar o contrato de estabilidade na sua base de código, poderá considerar as coleções Kotlin como estáveis adicionando kotlin.collections.* ao arquivo de configuração de estabilidade.

Coleção imutável

Para garantir a segurança da imutabilidade durante a compilação, você pode 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, você pode criar sua própria. 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>
)

Em seguida, use isso como o tipo de 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 ignorar 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 consideradas estáveis pode ser fornecido durante a compilação. Isso permite considerar as 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, caracteres curinga simples e duplos são aceitos. Um exemplo de configuração é mostrado abaixo:

// 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 em cada módulo do projeto separadamente, você pode fornecer diferentes configurações para diferentes módulos, 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 multimódulo. O compilador do Compose só poderá 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, esse pode ser um problema.

Solução

Para resolver esse problema, use 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 a adição de uma dependência do Compose à camada de dados. No entanto, é apenas a dependência do ambiente de execução do Compose, e não para 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 vai ocorrer 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 que podem ser compostos puláveis. Uma tentativa de fazer isso pode levar a uma otimização prematura que apresenta mais problemas do que corrige.

Há muitas situações em que ser pulável não tem nenhum benefício real e pode levar à difícil manutenção do código. Por 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 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 fazer com que a função de composição seja não reiniciável nos casos em que você determina que isso é mais overhead do que vale a pena.