Capítulo 03: Jetpack Compose Fundamentos, Composable Functions e Recomposição

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual da Arquitetura do Jetpack Compose

flowchart TD
    STATE["Estado Imutável (State<T>)"] -->|"Entrada na Função"| COMP["@Composable Function f(State)"]
    COMP -->|"Gera / Modifica"| SLOT["Slot Table (Árvore de Composição em Memória)"]

    subgraph ENGINE["Compose Runtime & Compiler"]
        SLOT --> DIFF["Algoritmo de Recomposição Inteligente (Skipping)"]
        DIFF -->|"Se Parâmetros Estáveis Iguais"| SKIP["Pular Execução (Skip Recomposition)"]
        DIFF -->|"Se Estado Alterado"| EXEC["Re-executar Sub-árvore Afetada"]
    end

    EXEC --> LAYOUT["Fase de Layout (Medição e Posicionamento)"]
    LAYOUT --> DRAW["Fase de Desenho (Canvas & RenderNode Android)"]
    DRAW --> SCREEN["Tela do Dispositivo (60 / 120 FPS)"]

    USER["Interação do Usuário (Toque / Clique)"] -->|"Dispara Evento"| MUTATE["Mutação de Estado"]
    MUTATE --> STATE

    style STATE fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style COMP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style SLOT fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style SKIP fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style EXEC fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Paradigma Declarativo e o Princípio $UI = f(State)$

Por mais de uma década, o desenvolvimento de telas no Android foi fundamentado no padrão imperativo baseado no framework de Views (android.view.View). Nesse modelo, os desenvolvedores criavam árvores de componentes em XML e manipulavam propriedades diretamente via código Java/Kotlin (findViewById, setText, setVisibility). Esse modelo gerava dessincronização contínua entre os dados de negócio e o estado visual renderizado.

O Jetpack Compose substitui essa abordagem por um paradigma estritamente funcional e declarativo:

  1. $UI = f(State)$: A interface não é uma estrutura mutável de objetos persistentes; ela é o resultado direto da execução de funções puras anotadas com @Composable. Quando o estado subjacente se altera, a função é reinvocada automaticamente pelo runtime.
  2. Recomposição Inteligente e Skipping: O compilador do Compose analisa a estabilidade dos tipos passados como argumentos para cada função @Composable. Se todos os parâmetros forem estáveis e seus valores não tiverem mudado desde a última execução, o Compose ignora a chamada daquela função inteira (Skipping), economizando preciosos milissegundos de processamento na thread principal.
  3. A Estrutura Slot Table: Por baixo dos panos, o runtime do Compose mantém uma estrutura de dados semelhante a um buffer com fendas (Slot Table). Ela armazena o histórico da árvore de composição, instâncias de remember, lambdas e nós de layout, permitindo inserções, remoções e atualizações com complexidade temporal amortizada $O(1)$.

📊 2. Comparativo Conceitual: Android Views Imperativas vs Jetpack Compose

Aspecto de Engenharia Android Views (XML + View Model Legado) Jetpack Compose (Kotlin Declarativo)
Definição de UI Arquivos XML estáticos com parse inflado em runtime Funções Kotlin anotadas com @Composable
Atualização de Tela Mutações manuais imperativas (textView.text = "novo") Recomposição automática orientada por fluxo de estado
Controle de Estado Estado distribuído e duplicado entre View e Controller Fonte única da verdade (Single Source of Truth) com State Hoisting
Reaproveitamento de Componentes Herança pesada (class CustomWidget : View(...)) Composição pura via funções e Modifiers encadeáveis
Tema e Estilização Atributos espalhados em styles.xml e seletores complexos MaterialTheme provido via CompositionLocalProvider com tipagem forte
Depuração & Preview Preview limitado e lento com rendering incompleto @Preview interativo direto no Android Studio com hot reload

💻 3. Laboratório Prático: Composable Puro com State Hoisting e Recomposição

O exemplo a seguir demonstra a criação de um componente desacoplado seguindo o padrão de State Hoisting (elevação de estado), tornando a função previsível, testável e reutilizável:

package br.com.portal.mobile.android.cap03

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// 1. Componente Puro (Stateless): Recebe dados e emite eventos via lambdas
@Composable
fun ContadorControle(
    rotulo: String,
    quantidade: Int,
    limiteMaximo: Int,
    onIncrementar: () -> Unit,
    onDecrementar: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(12.dp),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        )
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = rotulo,
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Bold
            )

            Spacer(modifier = Modifier.height(8.dp))

            Text(
                text = "$quantidade",
                fontSize = 36.sp,
                fontWeight = FontWeight.Black,
                color = if (quantidade >= limiteMaximo) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
            )

            Spacer(modifier = Modifier.height(12.dp))

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally)
            ) {
                OutlinedButton(
                    onClick = onDecrementar,
                    enabled = quantidade > 0
                ) {
                    Text("Diminuir (-)")
                }

                Button(
                    onClick = onIncrementar,
                    enabled = quantidade < limiteMaximo
                ) {
                    Text("Aumentar (+)")
                }
            }
        }
    }
}

⚠️ 4. Armadilhas Comuns no Ciclo de Vida do Compose

  1. Efeitos Colaterais Dentro do Corpo da Composable: Nunca execute chamadas de rede, leitura em disco ou mutação de variáveis globais diretamente no corpo da função composable. Como a função pode ser re-executada dezenas de vezes por segundo durante uma animação, use sempre LaunchedEffect ou SideEffect.
  2. Parâmetros Instáveis Causando Recomposição Desnecessária: Passar tipos mutáveis (java.util.ArrayList) ou classes de bibliotecas externas sem @Immutable faz o compilador do Compose tratar a função como instável, desabilitando o Skipping e degradando a taxa de quadros (FPS).
  3. Instanciação Pesada sem remember: Criar formatadores como SimpleDateFormat() ou expressões regulares dentro da Composable sem envolver em remember { ... } causa criação repetitiva de objetos a cada frame.