Capítulo 03: Jetpack Compose Fundamentos, Composable Functions e Recomposição
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:
- $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. - 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. - 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
- 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
LaunchedEffectouSideEffect. - Parâmetros Instáveis Causando Recomposição Desnecessária: Passar tipos mutáveis (
java.util.ArrayList) ou classes de bibliotecas externas sem@Immutablefaz o compilador do Compose tratar a função como instável, desabilitando o Skipping e degradando a taxa de quadros (FPS). - Instanciação Pesada sem
remember: Criar formatadores comoSimpleDateFormat()ou expressões regulares dentro da Composable sem envolver emremember { ... }causa criação repetitiva de objetos a cada frame.