Guia de Engenharia Android e Mobile UI/UX
Um framework operacional prático e testado em produção para projetar e construir aplicativos Android nativos com Jetpack Compose, Material 3, resiliência offline-first e ergonomia móvel centrada no ser humano.
1. Ergonomia Móvel Principal e Zona do Polegar
Dispositivos móveis são segurados com uma mão mais de 70% do tempo. Projete interfaces considerando o alcance do polegar, feedback tátil e atrito físico mínimo.
1.1 A Hierarquia de Alcance Ergonômico
+---------------------------+ <- Topo da Tela (Apenas Exibição)
| Status e Cabeçalhos | Mais difícil de alcançar. Reserve para status
| Título, Medidor de Cota | passivo, barras de busca e medidores ativos.
+---------------------------+
| | <- Zona de Alcance Médio
| Conteúdo Rolável | Ideal para itens de lista, detalhes e
| Cartões de Caixa de Entrada, Trechos | cartões de conteúdo navegável.
| |
+---------------------------+
| Alvos de Ação Principal | <- Zona Principal do Polegar (40% Inferior)
| Botões de Envio, Abas | Mais fácil de tocar. Mantenha navegação, CTAs
| Folhas Inferiores, Filtros | e controles interativos aqui.
+---------------------------+
1.2 Padrões Inegociáveis de Alvos de Toque
- Alvo Interativo Mínimo: Cada elemento clicável deve atender ou exceder $48 \times 48\text{dp}$ (
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)), mesmo que o ícone visual seja $24\text{dp}$. - Insetos de Borda e Áreas Seguras: Sempre envolva layouts raiz do Compose em
Scaffoldou apliqueModifier.windowInsetsPadding(WindowInsets.safeDrawing)para considerar recortes de câmera, gestos e barras de navegação. - Controles de Formulário na Parte Inferior: Coloque entradas de texto, botões de envio e diálogos de confirmação em
ModalBottomSheetou cartões fixados na parte inferior para que o teclado virtual (imePadding()) não obscureça o campo ativo.
2. Velocidade e Performance Percebida
Usuários móveis percebem atraso imediatamente. Nunca bloqueie a navegação ou mostre estados de carregamento vazios quando dados buscados anteriormente estiverem disponíveis.
2.1 A Regra Zero-Skeleton (Stale-While-Revalidate)
- O Problema: Mostrar um esqueleto de carregamento de tela cheia ou um spinner circular toda vez que o usuário toca na aba da Caixa de Entrada parece lento, chocante e sem polimento.
- A Solução: Implemente um padrão de cache Stale-While-Revalidate (SWR):
- Armazene itens buscados em armazenamento local criptografado (
EncryptedSharedPreferencesou SQLite/Room). - Ao entrar na tela, renderize itens em cache imediatamente (atraso 0ms).
- Revalide em segundo plano se o cache for mais antigo que 5–10 minutos.
- Quando os dados atualizados chegarem, anexe/atualize itens suavemente sem limpar a lista ou mostrar placeholders vazios.
- Mostre apenas indicadores sutis (ex.: um spinner de pull-to-refresh ou uma pequena barra de progresso no topo) durante atualizações em segundo plano.
- Armazene itens buscados em armazenamento local criptografado (
2.2 Padrão Offline-First Outbox
Nunca falhe uma ação do usuário porque o dispositivo móvel tem uma conexão celular instável ou está temporariamente no modo avião.
flowchart TD
UserAction["Usuário Compartilha Arquivo / Digita Nota"] --> LocalDraft["1. Salvar em Rascunhos Locais Outbox"]
LocalDraft --> RenderOutbox["2. Renderizar no Cartão da Fila Outbox Imediatamente"]
RenderOutbox --> CheckNet{"Conectado à Rede?"}
CheckNet -- Sim --> DirectUpload["3. Enviar Diretamente"]
CheckNet -- Não --> EnqueueWork["3. Enfileirar Trabalho WorkManager (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Aguardar Wi-Fi / Celular"]
WaitConn --> AutoDispatch["5. Envio em Segundo Plano com Backoff Exponencial"]
DirectUpload --> Success["6. Atualizar Estado da UI & Notificar Toast"]
AutoDispatch --> Success
Estados de UI do Outbox:
- Pendente: "Aguardando rede…" com um ícone de nuvem pausada.
- Enviando: "Enviando 42%…" com um indicador de progresso linear.
- Falhou / Tentando Novamente: Indicador de status claro com botões explícitos Tentar Novamente e Descartar.
3. Movimento, Transições e Háptica Tátil
O design moderno do Android (Android 14/15+) requer movimento dinâmico baseado em física e feedback sensorial tátil para parecer nativo e responsivo.
3.1 Manipulação de Borda a Borda e Barras do Sistema
Sempre desenhe atrás das barras do sistema globalmente em MainActivity.kt:
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 Gestos de Voltar Preditivos
Habilite a volta preditiva em AndroidManifest.xml (android:enableOnBackInvokedCallback="true") e use PredictiveBackHandler para folhas inferiores e gavetas de detalhes:
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// Escala ou desloca a folha de detalhes proporcional a backEvent.progress (0f até 1f)
detailScale = 1f - (backEvent.progress * 0.1f)
}
// Usuário confirmou o gesto de voltar
closeDetail()
} catch (e: CancellationException) {
// Usuário abortou o gesto de voltar, retorna suavemente para 1f
detailScale = 1f
}
}
3.3 Transições de Elementos Compartilhados
Ao transitar entre uma linha de lista da Caixa de Entrada e uma visualização de detalhes do item, envolva as telas em SharedTransitionLayout:
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
Use .sharedElement() ou .sharedBounds() em cartões de miniatura, ícones de avatar e títulos para criar transições contínuas.
3.4 Háptica Tátil
Nunca deixe ações do usuário silenciosas quando um marco físico ocorrer:
- Deslizes de Limiar (Swipe-to-Dismiss): Dispare
HapticFeedbackConstantsCompat.GESTURE_STARTquando o deslize cruzar o limiar de ação. - Copiar para Área de Transferência / Envio Concluído: Dispare
HapticFeedbackConstantsCompat.CONFIRM. - Falha de Validação / Erro de Rede: Dispare
HapticFeedbackConstantsCompat.REJECT.
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 Física de Mola sobre Easing Linear
Evite animações lineares rígidas. Sempre use física de mola para gestos, folhas e descartes:
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. Ações Inteligentes com Um Toque e Integração com o Sistema
Interfaces móveis devem minimizar a carga cognitiva reconhecendo o tipo de payload e oferecendo a ação nativa do sistema mais relevante.
4.1 Matriz de Ações Sensíveis ao Contexto
| Tipo de Payload | Ação Primária | Ação Secundária | Integração com o Sistema |
|---|---|---|---|
| URL / Link da Web | Abrir no Navegador (Intent.ACTION_VIEW) | Copiar Link | Chrome Custom Tabs ou navegador padrão do sistema |
| Texto / Trecho | Copiar Texto | Compartilhar (Intent.ACTION_SEND) | ClipboardManager do Android com Toast / Snackbar instantâneo |
| Arquivo / Mídia | Baixar para Pasta | Abrir Arquivo | Storage Access Framework (SAF) + FileProvider |
4.2 Downloads Diretos de Arquivos com Diretório Padrão Personalizado
Solicitar ao usuário o seletor de arquivos do sistema toda vez que ele baixar um arquivo cria atrito desnecessário.
- Permita que o usuário selecione uma Pasta de Download Padrão uma vez em Configurações/Central usando
ActivityResultContracts.OpenDocumentTree(). - Persista a permissão permanentemente:
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - Ao tocar em Download:
- Se um diretório padrão estiver definido: Transmita bytes diretamente usando
DocumentFile.createFile(), mostre o progresso ao vivo do byte e dispare um Toast nativo de conclusão ("document.pdfbaixado para Vault"). - Se nenhum padrão estiver definido: Recorra a
ActivityResultContracts.CreateDocument().
- Se um diretório padrão estiver definido: Transmita bytes diretamente usando
4.3 Abertura Segura de Arquivos com FileProvider
Nunca use URIs inseguros file:// crus. Sempre delegue a visualização de arquivos para aplicativos Android nativos usando FileProvider:
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "Abrir com..."))
5. Autenticação Móvel Sem Atrito
Digitar URLs de servidor e senhas longas em telas sensíveis ao toque é propenso a erros de digitação.
5.1 Hierarquia de Prioridade de Login
- Prioridade 1: Leitura de Código QR com Câmera (Emparelhamento de 1 Segundo)
- Exiba um código QR de emparelhamento temporário e assinado no aplicativo web para desktop.
- O usuário móvel abre o aplicativo, toca em Ler Código QR e aponta a câmera para a tela.
- O aplicativo analisa origem, token e nome do dispositivo em uma única passagem—autenticado instantaneamente.
- Prioridade 2: Nome de Usuário e Senha com URL do Servidor Recolhível
- 99% dos logins usam o host configurado padrão.
- Apresente apenas os campos Nome de Usuário e Senha por padrão.
- Oculte a URL do servidor atrás de uma alternância recolhível sutil:
"Configuração do servidor (avançado)".
5.2 Persistência de Sessão Deslizante de 14 Dias
- Armazene tokens de acesso (
va1_) e atualização (vr1_) noEncryptedSharedPreferencesdo Android. - Deslize/rote automaticamente o token de atualização em uso ativo.
- Enquanto o usuário abrir o aplicativo dentro de 14 dias, ele permanecerá logado sem precisar se autenticar novamente.
6. Layouts Adaptáveis (Dobráveis e Tablets)
Construa telas que escalam suavemente entre classes de tamanho de janela usando WindowSizeClass:
- Compact (
<600dp): Coluna única, barra de navegação inferior, folhas inferiores modais. - Medium (
600dp - 840dp): Trilho de navegação, grade de duas colunas. - Expanded (
>840dp): Layout persistente de mestre-detalhe em dois painéis lado a lado (Lista à esquerda, inspetor à direita).
7. Contraste de Cor e Acessibilidade de Estado do Material 3 (WCAG 2.2 AA / AAA)
Dispositivos móveis são particularmente propensos a falhas de contraste devido à iluminação ambiente variável (luz solar direta, ofuscamento externo, escurecimento para economia de bateria OLED). Busque contraste de texto WCAG 2.2 AAA ($\ge 7:1$) e conformidade estrita com WCAG 1.4.11 (contraste de borda não texto $\ge 3:1$) e WCAG 1.4.1 (uso de cor).
7.1 A Armadilha do FilterChip do Material 3 em Modo Escuro
- O Defeito: Por padrão, o
FilterChipdo Material 3 em modo escuro usasecondaryContainer(#37474F) como fundo selecionado contra asurfaceescura (#14181E). Isso produz uma taxa de contraste inaceitável de 1,83:1. Em baixo brilho ou ofuscamento, os usuários não conseguem distinguir qual chip está ativo. - A Correção:
- Imponha definição de borda de alto contraste com
selectedBorderColor = MaterialTheme.colorScheme.primary(que produz uma taxa de contraste de 9,88:1 em modo escuro). - Defina
selectedBorderWidth = 1.5.dp. - Cumpra WCAG 1.4.1 (estados não dependentes apenas de cor) fornecendo um ícone de marca de seleção principal:
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
- Imponha definição de borda de alto contraste com
7.2 Padrões de Legibilidade sob Luz Solar e Ofuscamento
- Texto Primário vs Superfície: Padronize o modo escuro com texto de alto contraste (
onSurface#E1E2E8ou#F8FAFCem#14181E), alcançando $\ge 13:1$ de contraste (excedendo WCAG AAA). - Contornos de Controles de Formulário:
OutlinedTextFielde bordas de rádio/checkbox devem usar tokensoutlineque mantenham pelo menos $3:1$ de contraste contra cartões de contêiner (Color(0xFF8D9199)ourgba(255, 255, 255, 0.20)explícito). - Linhas de Preset Compacto: Ao dispor linhas de chips compactos (ex.: presets de retenção
1h,4h,12h,1d), preserve tamanhos de alvo e apliquemaxLines = 1comselectedBorderColorpara que os chips nunca quebrem ou pareçam ambíguos.
8. Repositórios de Código Aberto de Referência
Inspecione estes projetos Android de código aberto líderes do setor para inspiração de design e implementação:
android/nowinandroid: Amostra emblemática do Google de Material 3 Compose demonstrando temas dinâmicos, layouts adaptáveis e arquitetura limpa.skydoves/Pokedex-Compose: Transições de elementos compartilhados de nível de produção (.sharedBounds/.sharedElement) e gestos suaves de folha de detalhes.plainhub/plain-app: Implementação notável de gerenciamento local de telefone, temas Material You e sincronização responsiva cliente-servidor.Gurupreet/ComposeCookBook: Catálogo exaustivo de animações de produção do Compose, barras inferiores e micro-interações de canvas personalizado.signalapp/Signal-Android: Padrão ouro para armazenamento local criptografado, workers de outbox em segundo plano e resiliência de rede.