Capítulo 07: Modifiers Avançados, Layout Phases e Custom Layouts

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


🗺️ Mapa Conceitual das Fases de Execução do Compose e Cadeia de Modifiers

flowchart TD
    subgraph PHASES["As 3 Fases do Pipeline do Compose"]
        P1["1. Fase de Composição: Constrói a árvore de nós da UI"]
        P2["2. Fase de Layout: Mede dimensões (Constraints) e posiciona nós (Coordinates)"]
        P3["3. Fase de Desenho: Gravação de comandos de renderização na GPU (Canvas)"]
        P1 --> P2 --> P3
    end

    subgraph MODIFIERS["A Regra da Ordem dos Modifiers (Top-Down Execution)"]
        M1["Modifier.size(100.dp) -> Define limites de medição"]
        M2["Modifier.background(Color.Blue) -> Pinta fundo nos limites atuais"]
        M3["Modifier.padding(16.dp) -> Encolhe constraints para filhos internos"]
        M4["Modifier.clickable { } -> Efeito ripple e área de toque restrita"]
        M1 --> M2 --> M3 --> M4
    end

    subgraph CUSTOM["Layout Customizado (Layout Composable)"]
        MEASURE["MeasureScope: Medição dos Measurables"]
        PLACE["Placeable: Posicionamento manual no plano cartesiano (X, Y)"]
        MEASURE --> PLACE
    end

    P2 -.-> MODIFIERS
    P2 -.-> CUSTOM

    style P1 fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style P2 fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style P3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style M2 fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style PLACE fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Papel dos Modifiers e a Arquitetura de Fases

No Jetpack Compose, os componentes não possuem dezenas de propriedades internas de estilização como margens, bordas ou alinhamentos embutidas na sua assinatura. Em vez disso, todo o comportamento extrínseco e apresentação visual é delegado ao objeto Modifier:

  1. A Ordem Importa Radicalmente: Modifiers operam como uma cadeia encadeada de decoração aplicada de fora para dentro. Um Modifier.padding(16.dp).background(Color.Red) desenhará o retângulo vermelho após aplicar o espaçamento (criando o efeito de margem externa). Inverter para Modifier.background(Color.Red).padding(16.dp) fará com que o fundo vermelho preencha todo o espaço e o padding empurre o conteúdo interno (efeito de preenchimento interno).
  2. As Três Fases de Execução: Para otimizar a renderização, o Compose divide o trabalho em Composição (o que mostrar), Layout (onde e com qual tamanho colocar) e Desenho (como pintar). Se um valor muda apenas na fase de desenho (por exemplo, rotação ou transparência via Modifier.graphicsLayer {}), o Compose pula completamente as fases de Composição e Layout, garantindo fluidez a 120 FPS.
  3. Construção de Custom Layouts: Quando contêineres padrões como Column, Row ou Box não atendem a requisitos complexos (como gráficos circulares, layouts de cascata ou fluxos em tag-cloud), o Compose disponibiliza o composable de baixo nível Layout().

📊 2. Comparativo de Impacto nas Fases de Execução

Modificador / Técnica Fase de Disparo Impacto na Performance Recomendação de Uso
Modifier.offset(x, y) Fase de Layout Recalcula medição e posicionamento a cada frame Mudanças ocasionais de posição estática
Modifier.offset { IntOffset(x, y) } Fase de Layout (sem recompor) Pula Composição, executa apenas medição Animações de translação baseadas em scroll ou gestos
Modifier.graphicsLayer { alpha; rotationZ } Fase de Desenho Puro Pula Composição e Layout; desenha direto via hardware Animações contínuas de rotação, fade ou escala
Modifier.background(Color) Fase de Desenho Rápido, mas recompõe se a cor mudar Cores de fundo estáticas
Modifier.drawBehind { drawRect(...) } Fase de Desenho Pinta no canvas sem alocar objetos no runtime Gráficos geométricos customizados e sombras

💻 3. Laboratório Prático: Custom Layout em Cascata (Flow/Cascade)

O código abaixo demonstra a criação de um layout customizado usando a função Layout do Compose para organizar itens sequencialmente com deslocamento diagonal escalonado:

package br.com.portal.mobile.android.cap07

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// 1. Layout Customizado: Escadinha em Cascata
@Composable
fun EscadaDiagonalLayout(
    espacamentoHorizontal: Dp = 16.dp,
    deslocamentoVertical: Dp = 20.dp,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Mede cada filho respeitando as constraints do pai
        val placeables: List<Placeable> = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        val stepX = espacamentoHorizontal.roundToPx()
        val stepY = deslocamentoVertical.roundToPx()

        // Calcula a largura e altura total acumulada pelo layout
        val larguraTotal = placeables.mapIndexed { idx, p -> idx * stepX + p.width }.maxOrNull() ?: 0
        val alturaTotal = placeables.mapIndexed { idx, p -> idx * stepY + p.height }.maxOrNull() ?: 0

        // Define as dimensões do componente pai e posiciona cada filho
        layout(larguraTotal, alturaTotal) {
            var posX = 0
            var posY = 0

            placeables.forEach { placeable ->
                placeable.placeRelative(x = posX, y = posY)
                posX += stepX
                posY += stepY
            }
        }
    }
}

// 2. Uso Prático na Interface
@Composable
fun DemonstracaoLayoutCascata() {
    EscadaDiagonalLayout(
        espacamentoHorizontal = 24.dp,
        deslocamentoVertical = 28.dp,
        modifier = Modifier.padding(16.dp)
    ) {
        BadgePasso(numero = "1", cor = Color(0xFF1976D2))
        BadgePasso(numero = "2", cor = Color(0xFF388E3C))
        BadgePasso(numero = "3", cor = Color(0xFFF57C00))
        BadgePasso(numero = "4", cor = Color(0xFF7B1FA2))
    }
}

@Composable
fun BadgePasso(numero: String, cor: Color) {
    Box(
        modifier = Modifier
            .size(54.dp)
            .background(cor, shape = RoundedCornerShape(12.dp)),
        contentAlignment = androidx.compose.ui.Alignment.Center
    ) {
        Text(text = numero, color = Color.White, fontSize = 20.sp)
    }
}

⚠️ 4. Armadilhas Comuns no Encadeamento de Modifiers

  1. Colocar clickable Após padding: Se você aplicar clickable depois de um padding, a área de clique será limitada apenas à área reduzida interna do componente, gerando sensação de falta de responsividade ao toque do usuário. Sempre declare o clickable antes do padding para ampliar a zona de toque acessível.
  2. Re-criar Modifiers Desnecessariamente em Funções Utilitárias: Ao criar funções que retornam Modifier, evite encadear instâncias novas a cada recomposição. Prefira estender Modifier criando extensões simples ou usando composed {} apenas quando o modificador exigir estados com remember.
  3. Ignorar o Parâmetro modifier nos Componentes: Todo Composable que renderiza elementos visuais deve aceitar um parâmetro modifier: Modifier = Modifier como primeiro argumento opcional e aplicá-lo ao nó raiz. Omitir esse parâmetro impede que o componente receba constraints ou margens personalizadas de quem o consome.