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:

  1. mutableStateOf e Reatividade: Uma instância de MutableState<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.
  2. O Papel do remember: Como funções @Composable podem ser executadas diversas vezes por segundo, declarar uma variável simples var x = mutableStateOf(0) reinicializaria o valor a cada quadro. A função remember armazena o valor retornado no slot table da composição, preservando o valor entre sucessivas recomposições.
  3. rememberSaveable e Sobrevivência à Recriação: O remember preserva 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). O rememberSaveable serializa o estado automaticamente em um Bundle do 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

  1. Mutação Direta de Objetos em Coleções: Criar um estado remember { mutableStateOf(mutableListOf<Item>()) } e invocar lista.value.add(novoItem) NÃO dispara a recomposição, pois a referência do objeto continua idêntica. Utilize sempre mutableStateListOf<Item>() ou reatribua uma nova cópia imutável (lista.value = lista.value + novoItem).
  2. Esquecer o Delegate by: Ao declarar val estado = remember { mutableStateOf("") }, é necessário acessar estado.value. A sintaxe idiomática recomendada é utilizar a importação dos operadores de delegação import androidx.compose.runtime.getValue e setValue, permitindo escrever var texto by remember { mutableStateOf("") }.
  3. 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.