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 comodata classsão estáveis. - Tipos com pelo menos um
varou com um campo de tipo instável (comoList,Set,Mapdakotlin.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 (umvarmutá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,StateFlowcoletado comoState, etc.). É o caso típico de wrappers deViewModel.
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 comcom.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:
- Rode
./gradlew :app:assembleDebuge abra*-classes.txt— confirme que as classes-alvo viraramstable. - Abra
*-composables.txt— confirme que os composables afetados viraramskippable. - Escreva um Macrobenchmark de scroll e compare P95 de
frameDurationCpuMs. - Verifique que nenhum
@Immutableestá mentindo (grep porvarem classes anotadas). - Documente no
compose-stability.confpor 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.