Capítulo 04: Gerenciamento de Estado no Compose: remember, mutableStateOf e State Hoisting
Capítulo 04: Gerenciamento de Estado no Compose: remember, mutableStateOf e State Hoisting
Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose
🗺️ Mapa Conceitual de Gerenciamento de Estado e Ciclo de Sobrevivência
flowchart TD
subgraph RECOMP["Recomposição Simples (Ciclo de Vida do Nó Compose)"]
REM["remember { mutableStateOf(inicial) }"]
REM -->|"Retém durante Recomposição"| LIVE["Estado em Memória da Composição"]
end
subgraph CONFIG["Mudança de Configuração (Rotação de Tela / Dark Mode)"]
CONFIG_EVENT["Evento do Sistema: Recriação da Activity"]
REM -->|"PERDE O ESTADO (Reset)"| RESET["Estado Volta ao Valor Inicial"]
REMSAV["rememberSaveable { mutableStateOf(inicial) }"]
REMSAV -->|"Salva no SavedStateRegistry / Bundle"| PERSIST["Preserva Estado pós-recriação"]
end
subgraph UDF["Padrão Unidirectional Data Flow (UDF)"]
STATE_DOWN["Estado (State<T>) Flui para Baixo"]
EVENT_UP["Eventos / Lambdas Fluem para Cima"]
CONTAINER["Stateful Host (ViewModel ou Tela Principal)"]
LEAF["Stateless Composable (Componente Puro Reutilizável)"]
CONTAINER -->|"Passa Valor de Leitura"| STATE_DOWN
STATE_DOWN --> LEAF
LEAF -->|"Dispara Ação do Usuário"| EVENT_UP
EVENT_UP --> CONTAINER
end
style REM fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style REMSAV fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style CONTAINER fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style LEAF fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. A Natureza do Estado no Jetpack Compose
No Jetpack Compose, qualquer valor que muda ao longo do tempo e afeta o que é exibido ao usuário é considerado Estado. Compreender como reter, salvar e elevar esse estado é a fronteira entre um aplicativo instável e uma aplicação profissional e resiliente:
mutableStateOfe Reatividade: Uma instância deMutableState<T>é um contêiner observável integrado diretamente ao motor de recomposição. Quando a propriedade.valueé lida dentro de uma Composable, o Compose registra aquele escopo de execução como assinante daquele estado. Sempre que uma nova atribuição ocorre (estado.value = novo), todos os escopos leitores são marcados para invalidação e recomposição.- O Papel do
remember: Como funções@Composablepodem ser executadas diversas vezes por segundo, declarar uma variável simplesvar x = mutableStateOf(0)reinicializaria o valor a cada quadro. A funçãorememberarmazena o valor retornado no slot table da composição, preservando o valor entre sucessivas recomposições. rememberSaveablee Sobrevivência à Recriação: Orememberpreserva o estado enquanto a Composable estiver na árvore, mas descarta tudo caso a Activity do Android seja destruída (por exemplo, ao rotacionar a tela entre portrait/landscape ou quando o sistema operacional descarta a Activity em segundo plano para liberar memória). OrememberSaveableserializa o estado automaticamente em umBundledo Android, recuperando-o sem atrito na reconstrução.
📊 2. Matriz de Sobrevivência e Escopo do Estado
| Mecanismo de Estado | Sobrevive à Recomposição? | Sobrevive à Rotação de Tela? | Sobrevive à Morte do Processo pelo SO? | Caso de Uso Recomendado |
|---|---|---|---|---|
Variável Local (var x = 0) |
❌ Não | ❌ Não | ❌ Não | Cálculos temporários imediatos dentro da função |
remember { mutableStateOf() } |
✅ Sim | ❌ Não | ❌ Não | Estados puramente visuais e efêmeros (animações locais, abas ativas) |
rememberSaveable { mutableStateOf() } |
✅ Sim | ✅ Sim | ✅ Sim (se serializável) | Formulários simples, scroll de listas, estados de inputs de texto |
ViewModel + StateFlow |
✅ Sim | ✅ Sim | ❌ Não (sem SavedStateHandle) | Estado de negócio da tela, regras de domínio e chamadas de API |
SavedStateHandle no ViewModel |
✅ Sim | ✅ Sim | ✅ Sim | IDs de navegação e filtros essenciais contra encerramento de processo |
💻 3. Laboratório Prático: Formulário Reativo com rememberSaveable e State Hoisting
O exemplo a seguir demonstra um padrão profissional de State Hoisting, separando a Composable de gerenciamento de estado (Stateful) do componente puramente visual de apresentação (Stateless):
package br.com.portal.mobile.android.cap04
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
// 1. Componente Stateful: gerencia a persistência e orquestração do estado
@Composable
fun CadastroClienteScreen(
onSalvarCliente: (nome: String, idade: Int) -> Unit
) {
// Uso de delegate 'by' para acesso direto às propriedades
var nome by rememberSaveable { mutableStateOf("") }
var idadeTexto by rememberSaveable { mutableStateOf("") }
var mensagemErro by rememberSaveable { mutableStateOf<String?>(null) }
CadastroClienteForm(
nome = nome,
idade = idadeTexto,
erro = mensagemErro,
onNomeAlterado = {
nome = it
mensagemErro = null
},
onIdadeAlterada = {
idadeTexto = it
mensagemErro = null
},
onSubmeter = {
val idadeNum = idadeTexto.toIntOrNull()
if (nome.isBlank()) {
mensagemErro = "O nome é obrigatório."
} else if (idadeNum == null || idadeNum <= 0) {
mensagemErro = "Informe uma idade válida maior que zero."
} else {
onSalvarCliente(nome, idadeNum)
}
}
)
}
// 2. Componente Stateless (Puro): totalmente desacoplado e fácil de testar
@Composable
fun CadastroClienteForm(
nome: String,
idade: String,
erro: String?,
onNomeAlterado: (String) -> Unit,
onIdadeAlterada: (String) -> Unit,
onSubmeter: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Novo Cadastro de Cliente",
style = MaterialTheme.typography.titleLarge
)
OutlinedTextField(
value = nome,
onValueChange = onNomeAlterado,
label = { Text("Nome Completo") },
isError = erro != null && nome.isBlank(),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = idade,
onValueChange = onIdadeAlterada,
label = { Text("Idade") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
isError = erro != null && idade.toIntOrNull() == null,
modifier = Modifier.fillMaxWidth()
)
if (erro != null) {
Text(
text = erro,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onSubmeter,
modifier = Modifier.fillMaxWidth()
) {
Text("Confirmar Cadastro")
}
}
}
⚠️ 4. Armadilhas Comuns no Manejo de Estado
- Mutação Direta de Objetos em Coleções: Criar um estado
remember { mutableStateOf(mutableListOf<Item>()) }e invocarlista.value.add(novoItem)NÃO dispara a recomposição, pois a referência do objeto continua idêntica. Utilize sempremutableStateListOf<Item>()ou reatribua uma nova cópia imutável (lista.value = lista.value + novoItem). - Esquecer o Delegate
by: Ao declararval estado = remember { mutableStateOf("") }, é necessário acessarestado.value. A sintaxe idiomática recomendada é utilizar a importação dos operadores de delegaçãoimport androidx.compose.runtime.getValueesetValue, permitindo escrevervar texto by remember { mutableStateOf("") }. - Passar ViewModels para Componentes de Baixo Nível: Passar a instância do ViewModel para subcomponentes que apenas desenham botões ou cards quebra o encapsulamento e impede a visualização em
@Preview. Pratique sempre o State Hoisting extraindo valores primitivos e lambdas de callback.