Recomposição desnecessária é o problema de performance número um em apps Jetpack Compose em produção. O sintoma é clássico: a UI engasga, lista trava em scroll, mas o profiler não mostra nada óbvio — porque a causa raiz raramente é “código lento” e quase sempre é o Compose Compiler classificando como instável uma classe que deveria ser estável. Em 2026, com o Compose Compiler 1.6+ integrado ao Kotlin 2.2+, o Configurador de Estabilidade (Stability Configurator) e o arquivo stability_config.conf se tornaram a ferramenta oficial para corrigir esse problema sem espalhar anotações pelo código.

Neste guia você configura o estabilidade do Compose corretamente, do diagnóstico ao fix, com Kotlin 2.2 e Compose BOM 2026. Se você ainda não dominou os fundamentos de recomposição, vale revisar antes o guia de Jetpack Compose e o guia de performance em Kotlin, porque este artigo pressupõe que você já sabe o que é slot table e quando o Compose decide recompor.

1. Por que estabilidade importa

O Compose decide se um composable vai recompor comparando os parâmetros da chamada atual com a anterior. Para isso funcionar de forma barata, o compilador marca cada parâmetro como estável ou instável:

  • Estável: o tipo pode ser comparado por igualdade estrutural e o Compose confia que, se os parâmetros não mudaram, a recomposição é segura de pular.
  • Instável: o Compose não consegue provar que o valor não mudou, então recomputa sempre.

A consequência prática é direta: um único parâmetro instável em um composable grande força a recomposição inteira dele e de todos os filhos que não estiverem protegidos por uma chave explícita. Em uma lista com 50 itens, isso vira jank visível.

A regra clássica do compilador é simples, mas restritiva:

  • Tipos primitivos (Int, String, Boolean) são estáveis.
  • Tipos val-only com campos estáveis e declarados como data class são estáveis.
  • Tipos com pelo menos um var ou com um campo de tipo instável (como List, Set, Map da kotlin.collections) são marcados como instáveis, mesmo que você nunca os muta.

É essa última regra que causa a maioria dos problemas reais. Uma data class imutável que contém List<String> é instável para o compilador, porque List é uma interface que pode ter uma implementação mutável (MutableList) por baixo.

2. Diagnóstico: ativando o relatório de estabilidade

Antes de mexer em nada, ative o relatório do Compose Compiler para ver exatamente quais classes estão instáveis e quais composables estão recompondo demais. No build.gradle.kts do módulo:

android {
    composeCompiler {
        // Kotlin 2.2+: use a extensão composeCompiler do plugin
        reportsDestination = layout.buildDirectory.dir("compose_compiler")
        metricsDestination = layout.buildDirectory.dir("compose_compiler")
    }
}

Após um ./gradlew :app:assembleDebug, abra app/build/compose_compiler/*-classes.txt. Cada classe aparece assim:

stable class MinhaDataClassImutavel {
  val nome: String
  val idade: Int
}
unstable class MinhaListaClass {
  val itens: List<String>   // <-- instável porque List é instável
}

O arquivo *-composables.txt mostra, para cada composable, se ele é restartable, skippable e quais parâmetros são instáveis. Um composable restartable mas não skippable com parâmetro instável é o seu alvo de otimização.

3. Solução clássica: @Immutable e @Stable

A correção mais antiga (e ainda válida) é anotar a classe. O artigo sobre data classes e sealed classes em Kotlin cobre o design delas em profundidade; aqui o foco é a anotação de estabilidade.

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable

@Immutable
data class EstadoTela(
    val itens: List<String>,        // agora tratada como estável
    val selecionado: Boolean
)

@Stable
class ViewModelHolder(val estado: StateFlow<EstadoTela>)

A diferença entre as duas anotações:

  • @Immutable: você garante que nenhum campo muda após a construção. Se você mentir (um var mutável por baixo), o Compose vai exibir UI inconsistente. Use só em classes realmente imutáveis.
  • @Stable: a classe pode mudar, mas você garante que notifica o Compose (MutableState, StateFlow coletado como State, etc.). É o caso típico de wrappers de ViewModel.

O problema dessa abordagem é que ela espalha dependência do androidx.compose.runtime por módulos que não deveriam conhecer Compose (camada de domínio, de dados). É aí que entra o Configurador de Estabilidade.

4. Configurador de Estabilidade (stability_config.conf)

Desde o Compose Compiler 1.5.4 (e refinado até o 1.6.x de 2026), você pode declarar tipos estáveis em um arquivo de configuração, sem anotar o código-fonte. Crie compose-stability.conf na raiz do módulo:

// Considera estas classes estáveis para o Compose, sem anotação no código
java.time.Duration
java.time.Instant
java.time.LocalDate
java.util.UUID
kotlin.coroutines.CoroutineContext

// Pacote inteiro da camada de modelo pode ser marcado como estável
com.meuapp.modelo.*

E referencie no build.gradle.kts:

android {
    composeCompiler {
        stabilityConfigurationFile = project.layout.projectDirectory.file("compose-stability.conf")
    }
}

As regras aceitas no arquivo são:

  • Nome totalmente qualificado de uma classe: com.meuapp.modelo.Usuario.
  • Pacote curinga: com.meuapp.modelo.* (cobre todos os tipos do pacote, recursivamente em subpacotes com com.meuapp.modelo.**).
  • Comentários com # ou //.

Isso resolve o caso mais comum: você tem uma camada de modelo data class pura (sem var, sem MutableList) que o compilador marca como instável só porque contém List. Marcando o pacote como estável, todo o modelo passa a ser skippable sem poluir o código de domínio com anotação Android.

5. Caso prático: lista que travava no scroll

Considere um caso real. Você tem:

data class Produto(
    val id: String,
    val nome: String,
    val tags: List<String>   // <-- torna a classe instável
)

@Composable
fun ListaProdutos(produtos: List<Produto>) {
    LazyColumn {
        items(produtos, key = { it.id }) { produto ->
            ItemProduto(produto)   // recomputa a cada scroll por causa do tipo instável
        }
    }
}

@Composable
fun ItemProduto(produto: Produto) {
    // ...
}

Sem correção, o relatório mostra ItemProduto como restartable mas não skippable. A correção mínima é adicionar ao compose-stability.conf:

com.meuapp.modelo.*

Rode novamente o build, regenere o relatório e confirme que ItemProduto agora aparece como skippable. O efeito em uma lista de 50 itens costuma ser uma queda de 30 a 60% no tempo de recomposição medido pelo Macrobenchmark, porque o Compose passa a pular recomposições de itens que não mudaram.

6. Armadilhas comuns

6.1. List, Set, Map da kotlin.collections

Mesmo com @Immutable na classe que as contém, o compilador em algumas versões ainda reclama. A forma mais robusta em 2026 é usar o configurador no pacote do modelo, ou converter para tipos imutáveis do KotlinX (ImmutableList, PersistentList via kotlinx.collections.immutable), que o compilador reconhece como estáveis nativamente.

import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

data class Carrinho(val itens: ImmutableList<Produto> = persistentListOf())

6.2. Classes com List mutada internamente

Não minta para o compilador. Se você marcar uma classe como @Immutable mas mutar a lista por referência externa, o Compose não vai recompor quando deveria e a UI fica dessincronizada. Nesse caso, use @Stable com MutableState ou migre para ImmutableList.

6.3. Lambdas capturando estado

Um composable com parâmetro lambda (Produto) -> Unit é estável, mas se o lambda captura um var externo, a instabilidade volta pela porta dos fundos. Use remember { { produto -> ... } } ou extraia o estado para um rememberSaveable. Isso conecta com o padrão discutido no guia de Flow e StateFlow — o estado observável sempre deve vir de uma fonte estável.

6.4. ViewModel como parâmetro

Passar o ViewModel inteiro como parâmetro de composable deixa tudo instável. Extraia o estado em uma data class imutável e observe com collectAsStateWithLifecycle(). Veja o guia de arquitetura MVVM em Kotlin para o padrão de UI state.

7. Medindo o ganho com Macrobenchmark

Depois de aplicar o configurador, meça com Macrobenchmark para confirmar o ganho real, não só o relatório do compilador. Um benchmark de scroll típico:

@Test
fun scrollListaProdutos() = benchmarkRule.measureRepeated(
    packageName = "com.meuapp",
    metrics = listOf(FrameTimingMetric())
) {
    startActivityAndWait()
    val lista = device.findObject(By.res("lista_produtos"))
    device.scrollUntilFound(lista, UiScrollable.Direction.DOWN, 20)
}

Compare o frameDurationCpuMs P95 antes e depois. Reduções de 8-10ms para 4-5ms são comuns quando a causa raiz era instabilidade de parâmetro. Combine com o guia de Baseline Profiles para o ganho cumulativo.

8. Checklist final

Antes de fechar um PR de otimização de estabilidade:

  1. Rode ./gradlew :app:assembleDebug e abra *-classes.txt — confirme que as classes-alvo viraram stable.
  2. Abra *-composables.txt — confirme que os composables afetados viraram skippable.
  3. Escreva um Macrobenchmark de scroll e compare P95 de frameDurationCpuMs.
  4. Verifique que nenhum @Immutable está mentindo (grep por var em classes anotadas).
  5. Documente no compose-stability.conf por que cada pacote foi marcado — um comentário por linha economiza debug futuro.

Conclusão

O Configurador de Estabilidade é o investimento de maior ROI em performance de Compose para apps médios e grandes em 2026. Ele resolve na origem o problema de recomposição desnecessária sem acoplar a camada de domínio ao runtime do Compose, e o ganho é mensurável com as ferramentas que você já tem no build. A regra prática: se o relatório do compilador mostra uma data class pura marcada como unstable, o stability_config.conf é quase sempre a resposta certa.

Para aprofundar, combine este guia com o de Baseline Profiles e Macrobenchmark, o de Material 3 Expressive (que também depende de parâmetros estáveis para animar bem) e o guia de testes em Kotlin para validar o comportamento da UI após a mudança.