O que é State em Kotlin?
Resposta rápida: no Jetpack Compose, State<T> é um valor observável pela interface. Quando um Composable lê state.value e esse valor muda, o Compose agenda a recomposição do trecho dependente. Para estado local, use remember { mutableStateOf(...) } (ou mutableIntStateOf para primitivos); para estado simples que deve ser restaurado após recriação da Activity, use rememberSaveable; para converter Flow/I/O em State na UI, use produceState; para estado de tela e lógica de negócio, exponha StateFlow no ViewModel e colete com collectAsStateWithLifecycle() no Android.
@Composable
fun Contador() {
var contagem by remember { mutableIntStateOf(0) }
Button(onClick = { contagem++ }) {
Text("Cliques: $contagem")
}
}
O exemplo combina três peças:
mutableIntStateOf(0)cria um estado primitivo deIntsem boxing;remembermantém a mesma instância durante recomposições;byusa delegação de propriedade para ler e alterar o valor sem escrever.intValue.
O Compose não redesenha necessariamente a tela inteira. Ele registra onde o estado foi lido e pode ignorar partes que não dependem daquele valor.
State, MutableState e estado comum
Uma variável Kotlin comum não notifica o Compose:
@Composable
fun ContadorQuebrado() {
var contagem = 0
Button(onClick = { contagem++ }) {
Text("Cliques: $contagem")
}
}
O clique altera a variável, mas não há um estado observável que invalide a composição. Além disso, a variável é recriada quando a função recompõe.
Com MutableState<T>, a escrita pode acionar recomposição:
val contagem = remember { mutableIntStateOf(0) }
Button(onClick = { contagem.intValue++ }) {
Text("Cliques: ${contagem.intValue}")
}
State<T> oferece apenas leitura (value). MutableState<T> acrescenta escrita. Em componentes reutilizáveis, prefira receber o valor e eventos em vez de entregar o estado mutável diretamente.
mutableIntStateOf e estados primitivos
Para Int, Long, Float e Double, o Compose oferece holders especializados que evitam boxing em cada leitura/escrita:
| Tipo | API recomendada |
|---|---|
Int | mutableIntStateOf |
Long | mutableLongStateOf |
Float | mutableFloatStateOf |
Double | mutableDoubleStateOf |
| Outros / objetos | mutableStateOf |
@Composable
fun ProgressoLocal() {
var progresso by remember { mutableFloatStateOf(0f) }
var passos by remember { mutableIntStateOf(0) }
Slider(
value = progresso,
onValueChange = {
progresso = it
passos++
}
)
Text("Passos: $passos")
}
Com by, o delegate já expõe o primitivo (Int, Float, …). Sem by, use .intValue, .floatValue e equivalentes. Continue usando mutableStateOf para String, data classes, sealed hierarchies e qualquer tipo que não seja um desses quatro primitivos.
remember vs rememberSaveable vs ViewModel
| Necessidade | Opção comum |
|---|---|
| Sobreviver apenas a recomposições | remember |
| Restaurar estado simples de UI após recriação | rememberSaveable |
| Estado de uma tela com lógica de apresentação | ViewModel + StateFlow |
| Restaurar dados após morte do processo | SavedStateHandle ou persistência |
@Composable
fun Formulario() {
var filtroTemporario by remember { mutableStateOf("") }
var nomeDigitado by rememberSaveable { mutableStateOf("") }
Column {
TextField(
value = filtroTemporario,
onValueChange = { filtroTemporario = it },
label = { Text("Filtro temporário") }
)
TextField(
value = nomeDigitado,
onValueChange = { nomeDigitado = it },
label = { Text("Nome") }
)
}
}
rememberSaveable não transforma qualquer objeto em persistente. Tipos aceitos pelo Bundle funcionam diretamente; tipos próprios podem precisar de Saver. Também não use essa API para listas grandes ou dados que pertencem ao banco, à rede ou ao DataStore.
State hoisting: estado desce, eventos sobem
State hoisting (elevação de estado) move o estado para o chamador. O componente recebe o valor atual e um callback para comunicar mudanças:
@Composable
fun CampoDeNome(
nome: String,
onNomeChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = nome,
onValueChange = onNomeChange,
label = { Text("Nome") },
modifier = modifier.fillMaxWidth()
)
}
@Composable
fun Cadastro() {
var nome by rememberSaveable { mutableStateOf("") }
CampoDeNome(
nome = nome,
onNomeChange = { nome = it }
)
}
A dupla value + onValueChange é uma convenção importante no Compose. O filho fica stateless, reutilizável e fácil de testar; o pai vira a fonte única da verdade.
Eleve o estado pelo menos até o ancestral comum mais próximo de todos os componentes que precisam lê-lo ou alterá-lo. Não é necessário mover todo estado local para um ViewModel: abertura de menu, expansão de item e foco temporário podem continuar próximos da UI.
derivedStateOf: estado calculado
derivedStateOf é útil quando uma entrada muda com mais frequência do que a UI precisa atualizar. Um caso típico é mostrar um botão depois que a lista sai do primeiro item:
@Composable
fun ListaDeMensagens(mensagens: List<String>) {
val listState = rememberLazyListState()
val mostrarVoltarAoTopo by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
Box {
LazyColumn(state = listState) {
items(mensagens) { mensagem ->
Text(mensagem)
}
}
if (mostrarVoltarAoTopo) {
FloatingActionButton(onClick = { /* iniciar scroll em coroutine */ }) {
Text("↑")
}
}
}
}
O índice e o deslocamento da lista podem mudar muitas vezes durante o scroll, mas o booleano só alterna quando a condição muda. Não use derivedStateOf para toda concatenação ou cálculo barato: ele também tem custo e não é uma forma genérica de “memorizar” valores.
Estado de tela com ViewModel e StateFlow
Para telas reais, modele carregamento, sucesso e erro como um estado imutável. O ViewModel mantém a versão mutável privada e expõe somente leitura:
data class ListaUiState(
val carregando: Boolean = false,
val itens: List<String> = emptyList(),
val erro: String? = null
)
class ListaViewModel(
private val repositorio: ItensRepositorio
) : ViewModel() {
private val _uiState = MutableStateFlow(ListaUiState())
val uiState: StateFlow<ListaUiState> = _uiState.asStateFlow()
fun carregar() {
viewModelScope.launch {
_uiState.update { it.copy(carregando = true, erro = null) }
runCatching { repositorio.buscarItens() }
.onSuccess { itens ->
_uiState.value = ListaUiState(itens = itens)
}
.onFailure { causa ->
_uiState.value = ListaUiState(
erro = causa.message ?: "Não foi possível carregar"
)
}
}
}
}
Na tela Android com Compose, colete respeitando o ciclo de vida:
@Composable
fun ListaScreen(viewModel: ListaViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when {
uiState.carregando -> CircularProgressIndicator()
uiState.erro != null -> Text(uiState.erro!!)
else -> LazyColumn {
items(uiState.itens) { item -> Text(item) }
}
}
}
collectAsStateWithLifecycle() é a escolha recomendada para Compose no Android porque interrompe e retoma a coleta conforme o Lifecycle. Em Compose Multiplatform ou código sem Lifecycle Android, use collectAsState(). Para aprofundar a arquitetura, consulte o guia de MVVM com Kotlin e o guia de Kotlin Flow.
State, StateFlow, SharedFlow e LiveData
| Tipo | Característica | Uso típico |
|---|---|---|
State<T> | Observável pelo runtime do Compose | Valor lido pela UI Compose |
MutableState<T> | State com escrita | Estado local do Composable |
StateFlow<T> | Hot flow com valor atual | Estado de tela no ViewModel |
SharedFlow<T> | Hot flow configurável, sem exigir valor inicial | Distribuição de eventos ou valores compartilhados |
LiveData<T> | Observável integrado ao Lifecycle Android | Bases de código Android existentes |
StateFlow representa bem estado porque sempre possui um valor atual. Para navegação, snackbar e outros efeitos pontuais, não copie automaticamente o mesmo modelo: eventos exigem pensar em entrega, repetição e perda quando não há coletor.
Coleções e estabilidade do estado
Este código pode não recompor como esperado porque a referência da lista continua a mesma:
val itens = remember { mutableStateOf(mutableListOf("Kotlin")) }
itens.value.add("Compose") // mutação interna; não houve nova atribuição
Prefira estado imutável e atribua uma nova coleção:
var itens by remember { mutableStateOf(listOf("Kotlin")) }
itens = itens + "Compose"
Quando precisa de uma coleção mutável observável pelo Compose, existe mutableStateListOf, mas ainda vale evitar expor mutabilidade sem controle. Leia também o verbete sobre imutabilidade.
produceState: State a partir de trabalho assíncrono
produceState cria um State alimentado por uma coroutine dentro do Composable. É o caminho curto quando a UI precisa observar um Flow, callback ou I/O sem subir imediatamente para um ViewModel:
@Composable
fun PerfilRemoto(userId: String, repositorio: UsuarioRepositorio): State<UsuarioUi?> {
return produceState<UsuarioUi?>(initialValue = null, userId) {
value = repositorio.buscar(userId)
}
}
@Composable
fun PerfilScreen(userId: String, repositorio: UsuarioRepositorio) {
val usuario by PerfilRemoto(userId, repositorio)
if (usuario == null) {
CircularProgressIndicator()
} else {
Text(usuario!!.nome)
}
}
A chave userId cancela e reinicia o produtor quando muda. O bloco é cancelado automaticamente quando o Composable sai da composição.
Use produceState para adapters locais (Flow → State, listener → State). Para estado de tela com regras de negócio, testes e sobrevivência a configuração, continue no ViewModel com StateFlow. Não misture as duas fontes da verdade na mesma tela.
snapshotFlow: observando State como Flow
snapshotFlow converte leituras de estado do Compose em um Flow. Isso é útil para analytics, debounce ou operadores de Flow sem criar recomposição desnecessária:
@Composable
fun Busca(onBuscar: suspend (String) -> Unit) {
var query by rememberSaveable { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { query }
.debounce(300)
.distinctUntilChanged()
.collectLatest { texto ->
if (texto.length >= 3) onBuscar(texto)
}
}
TextField(value = query, onValueChange = { query = it })
}
O bloco de snapshotFlow deve apenas ler estado do Snapshot. Efeitos colaterais pertencem aos operadores seguintes ou ao coletor. Junto com produceState, forma o par State ↔ Flow: um transforma trabalho assíncrono em State; o outro observa State como Flow.
Erros comuns
- Criar
mutableStateOfsemrememberdentro do Composable: uma recomposição recria o holder e perde o valor. - Duplicar a fonte da verdade: manter uma cópia no filho e outra no ViewModel produz sincronização difícil. Eleve o estado.
- Expor
MutableStateFlowpublicamente: qualquer consumidor passa a poder alterar a tela. ExponhaStateFlowcomasStateFlow(). - Mutar
MutableListsem nova atribuição: a mudança interna pode não ser observada. Prefira coleções imutáveis ouSnapshotStateList. - Usar
rememberpara dados de negócio: a UI não deve ser o repositório de pedidos, usuários ou resultados de rede. - Achar que ViewModel sobrevive à morte do processo: ele sobrevive à recriação por configuração, não ao encerramento do app pelo sistema.
- Coletar Flow sem considerar o Lifecycle no Android: em telas Android, prefira
collectAsStateWithLifecycle(). - Usar
derivedStateOfcomo cache universal: aplique quando o resultado muda menos que as entradas observadas, não em qualquer cálculo. - Usar
mutableStateOf(0)para contadores: prefiramutableIntStateOf(e equivalentes) para evitar boxing desnecessário. - Colocar I/O pesado em
produceStatesem cancelamento claro: o produtor cancela ao sair da composição, mas regras de negócio complexas ainda pertencem ao ViewModel.
Perguntas frequentes
Toda mudança em State recompõe a tela inteira?
Não. O Compose rastreia os locais que leram o estado e agenda a recomposição dos escopos dependentes. Depois, ainda pode ignorar chamadas cujas entradas continuam estáveis e iguais.
StateFlow substitui mutableStateOf?
Não. Eles atuam bem em camadas diferentes. StateFlow é apropriado para o ViewModel e outras camadas baseadas em coroutines; MutableState é simples para estado local da UI Compose.
Devo colocar todo estado no ViewModel?
Não. Estado de negócio e estado de tela compartilhado costumam ficar no ViewModel. Estado efêmero de um componente — como um menu aberto — pode ficar no Composable se nenhuma outra parte precisar controlá-lo.
Qual a diferença entre estado e evento?
Estado descreve como a tela está agora e pode ser relido, como carregando = true. Evento representa algo que aconteceu, como “mostrar snackbar”. Tratar evento como estado sem uma estratégia de consumo pode repeti-lo após recriações.
mutableIntStateOf substitui mutableStateOf?
Só para os quatro primitivos suportados. Para String, listas, data classes e sealed types, continue com mutableStateOf. A escolha é de performance e clareza, não de semântica diferente de observação.
Quando preferir produceState em vez de ViewModel?
Quando o State é um adaptador local e curto (por exemplo, espelhar um Flow de sensor ou um callback de player). Se a tela tem regras de negócio, erros tipados, retry ou precisa de testes isolados, use ViewModel + StateFlow.
Próximos passos
- Composable — como funções de UI participam da composição
- ViewModel — ciclo de vida e estado de tela no Android
- Flow — streams assíncronos, StateFlow e operadores
- Modifier — composição de comportamento e layout na UI
- Imutabilidade — modelos previsíveis para estado
- Jetpack Compose básico — primeiro contato com UI declarativa
- MVVM com Kotlin (tutorial) — ViewModel, StateFlow e repository na prática
- Jetpack Compose vs XML — quando migrar e o que muda no estado
- Jetpack Compose: guia completo — UI declarativa do básico à arquitetura
- MVVM com Kotlin — ViewModel, repository e fluxo unidirecional
- Kotlin Flow na prática — coleta, transformação e compartilhamento
- Kotlin para Android — roadmap e setup atual do primeiro app