Guía de Ingeniería de Android y UI/UX Móvil
Un marco operativo práctico y probado en producción para diseñar y construir aplicaciones Android nativas con Jetpack Compose, Material 3, resiliencia offline-first y ergonomía móvil centrada en el ser humano.
1. Ergonomía Móvil Central y Zona del Pulgar
Los dispositivos móviles se sostienen con una sola mano más del 70% del tiempo. Diseña las interfaces en torno a la alcanzabilidad del pulgar, la retroalimentación táctil y la mínima fricción física.
1.1 Jerarquía de Alcanzabilidad Ergonómica
+---------------------------+ <- Parte superior de la pantalla (Solo visualización)
| Estado y Encabezados | Más difícil de alcanzar. Reserva para estado
| Título, Medidor de cuota | pasivo, barras de búsqueda y medidores en vivo.
+---------------------------+
| | <- Zona de Alcance Medio
| Contenido Desplazable | Ideal para elementos de lista, detalles y
| Tarjetas de Bandeja | tarjetas de contenido navegable.
| de Entrada, Fragmentos |
| |
+---------------------------+
| Objetivos de Acción | <- Zona Principal del Pulgar (40% inferior)
| Principal | Más fácil de tocar. Mantén la navegación, CTAs,
| Botones de Envío, | paneles inferiores, filtros y controles
| Pestañas, Hojas | interactivos aquí.
| Inferiores, Filtros |
+---------------------------+
1.2 Estándares No Negociables de Objetivos Táctiles
- Objetivo Interactivo Mínimo: Cada elemento cliqueable debe cumplir o superar $48 \times 48\text{dp}$ (
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)), incluso si el icono visual es de $24\text{dp}$. - Inserciones de Borde y Áreas Seguras: Siempre envuelve los diseños raíz de Compose en
Scaffoldo aplicaModifier.windowInsetsPadding(WindowInsets.safeDrawing)para tener en cuenta los recortes de la cámara, los gestos y las barras de navegación. - Controles de Formulario en la Parte Inferior: Coloca las entradas de texto, los botones de envío y los diálogos de confirmación en
ModalBottomSheeto tarjetas ancladas en la parte inferior para que el teclado del software (imePadding()) no oculte el campo activo.
2. Velocidad y Rendimiento Percibido
Los usuarios móviles perciben la demora de inmediato. Nunca bloquees la navegación ni muestres estados de carga vacíos cuando haya datos previamente obtenidos disponibles.
2.1 La Regla de Cero Esqueletos (Stale-While-Revalidate)
- El Problema: Mostrar un cargador de esqueleto de página completa o un spinner circular cada vez que el usuario toca la pestaña de la bandeja de entrada se siente lento, discordante y poco pulido.
- La Solución: Implementa un patrón de caché Stale-While-Revalidate (SWR):
- Almacena los elementos obtenidos en almacenamiento local cifrado (
EncryptedSharedPreferenceso SQLite/Room). - Al entrar en la pantalla, muestra los elementos en caché inmediatamente (sin demora).
- Revalida en segundo plano si la caché tiene más de 5 a 10 minutos.
- Cuando lleguen los datos actualizados, agrega/actualiza los elementos suavemente sin borrar la lista ni mostrar marcadores de posición vacíos.
- Solo muestra indicadores sutiles (por ejemplo, un spinner de pull-to-refresh o una pequeña barra de progreso superior) durante las actualizaciones en segundo plano.
- Almacena los elementos obtenidos en almacenamiento local cifrado (
2.2 Patrón de Buzón de Salida Offline-First
Nunca falles una acción del usuario porque el dispositivo móvil tenga una conexión celular intermitente o esté temporalmente en modo avión.
flowchart TD
UserAction["El usuario comparte un archivo / escribe una nota"] --> LocalDraft["1. Guardar en borradores locales del buzón de salida"]
LocalDraft --> RenderOutbox["2. Renderizar en la tarjeta de la cola del buzón de salida inmediatamente"]
RenderOutbox --> CheckNet{"¿Conectado a la red?"}
CheckNet -- Sí --> DirectUpload["3. Subir directamente"]
CheckNet -- No --> EnqueueWork["3. Encolar trabajo de WorkManager (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Esperar por Wi-Fi / Celular"]
WaitConn --> AutoDispatch["5. Subida en segundo plano con retroceso exponencial"]
DirectUpload --> Success["6. Actualizar estado de la IU y notificar Toast"]
AutoDispatch --> Success
Estados de la IU del Buzón de Salida:
- Pendiente: "Esperando red…" con un icono de nube en pausa.
- Subiendo: "Enviando 42%…" con un indicador de progreso lineal.
- Fallido / Reintentando: Indicador de estado claro con botones explícitos de Reintentar y Descartar.
3. Movimiento, Transiciones y Hápticas Táctiles
El diseño moderno de Android (Android 14/15+) requiere movimiento dinámico basado en física y retroalimentación sensorial táctil para sentirse nativo y receptivo.
3.1 Manejo de Borde a Borde y Barras del Sistema
Siempre dibuja detrás de las barras del sistema globalmente en MainActivity.kt:
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 Gestos de Retroceso Predictivos
Habilita el retroceso predictivo en AndroidManifest.xml (android:enableOnBackInvokedCallback="true") y usa PredictiveBackHandler para hojas inferiores y paneles de detalle:
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// Escalar o desplazar la hoja de detalle proporcionalmente a backEvent.progress (0f a 1f)
detailScale = 1f - (backEvent.progress * 0.1f)
}
// El usuario confirmó el gesto de retroceso
closeDetail()
} catch (e: CancellationException) {
// El usuario abortó el gesto de retroceso, volver a 1f con rebote
detailScale = 1f
}
}
3.3 Transiciones de Elementos Compartidos
Al hacer la transición entre una fila de la lista de la bandeja de entrada y una vista de detalle del elemento, envuelve las pantallas en SharedTransitionLayout:
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
Usa .sharedElement() o .sharedBounds() en tarjetas de miniatura, iconos de avatar y títulos para crear transiciones sin interrupciones.
3.4 Hápticas Táctiles
Nunca dejes las acciones del usuario en silencio cuando ocurra un hito físico:
- Deslizamientos de Umbral (Swipe-to-Dismiss): Activa
HapticFeedbackConstantsCompat.GESTURE_STARTcuando el deslizamiento cruce el umbral de acción. - Copiar al Portapapeles / Envío Completado: Activa
HapticFeedbackConstantsCompat.CONFIRM. - Fallo de Validación / Error de Red: Activa
HapticFeedbackConstantsCompat.REJECT.
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 Física de Resorte sobre Easing Lineal
Evita las animaciones lineales rígidas. Usa siempre física de resorte para gestos, hojas y descartes:
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. Acciones Inteligentes de un Solo Toque e Integración con el Sistema
Las interfaces móviles deben minimizar la carga cognitiva reconociendo el tipo de carga útil y ofreciendo la acción nativa del sistema más relevante.
4.1 Matriz de Acciones Sensibles al Contexto
| Tipo de Carga Útil | Acción Principal | Acción Secundaria | Integración con el Sistema |
|---|---|---|---|
| URL / Enlace Web | Abrir en el Navegador (Intent.ACTION_VIEW) | Copiar Enlace | Chrome Custom Tabs o navegador predeterminado del sistema |
| Texto / Fragmento | Copiar Texto | Compartir (Intent.ACTION_SEND) | ClipboardManager de Android con Toast / Snackbar instantáneo |
| Archivo / Multimedia | Descargar a Carpeta | Abrir Archivo | Marco de Acceso al Almacenamiento (SAF) + FileProvider |
4.2 Descargas Directas de Archivos con Directorio Predeterminado Personalizado
Solicitar al usuario el selector de archivos del sistema cada vez que descarga un archivo crea fricción innecesaria.
- Permite al usuario seleccionar una Carpeta de Descarga Predeterminada una vez en Ajustes/Centro usando
ActivityResultContracts.OpenDocumentTree(). - Persiste el permiso permanentemente:
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - Al tocar Descargar:
- Si hay un directorio predeterminado establecido: Transmite bytes directamente usando
DocumentFile.createFile(), muestra el progreso de bytes en vivo y dispara un Toast de finalización nativo ("Descargado documento.pdf en Vault"). - Si no hay un predeterminado: Recurre a
ActivityResultContracts.CreateDocument().
- Si hay un directorio predeterminado establecido: Transmite bytes directamente usando
4.3 Apertura Segura de Archivos con FileProvider
Nunca uses URIs inseguras de file:// en bruto. Siempre delega la visualización de archivos a aplicaciones nativas de Android 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 con..."))
5. Autenticación Móvil Sin Fricciones
Escribir URLs de servidor y contraseñas largas en pantallas táctiles móviles es propenso a errores tipográficos.
5.1 Jerarquía de Prioridad de Inicio de Sesión
- Prioridad 1: Escaneo de Código QR con Cámara (Emparejamiento de 1 Segundo)
- Muestra un código QR de emparejamiento firmado temporal en la aplicación web de escritorio.
- El usuario móvil abre la aplicación, toca Escanear Código QR, apunta la cámara a la pantalla.
- La aplicación analiza el origen, el token y el nombre del dispositivo en una sola pasada: autenticado al instante.
- Prioridad 2: Nombre de Usuario y Contraseña con URL de Servidor Plegable
- El 99% de los inicios de sesión usan el host configurado por defecto.
- Presenta solo los campos Nombre de Usuario y Contraseña por defecto.
- Oculta la URL del servidor detrás de un conmutador plegable sutil:
"Configuración del servidor (avanzado)".
5.2 Persistencia de Sesión Deslizante de 14 Días
- Almacena los tokens de acceso (
va1_) y actualización (vr1_) enEncryptedSharedPreferencesde Android. - Desliza/rota automáticamente el token de actualización con el uso activo.
- Mientras el usuario abra la aplicación dentro de los 14 días, permanecerá conectado sin necesidad de reautenticarse.
6. Diseños Adaptativos (Plegables y Tablets)
Construye pantallas que escalen suavemente entre clases de tamaño de ventana usando WindowSizeClass:
- Compacto (
<600dp): Una sola columna, barra de navegación inferior, hojas inferiores modales. - Medio (
600dp - 840dp): Riel de navegación, cuadrícula de dos columnas. - Expandido (
>840dp): Diseño persistente de dos paneles maestro-detalle lado a lado (Lista a la izquierda, inspector a la derecha).
7. Contraste de Color de Material 3 y Accesibilidad de Estados (WCAG 2.2 AA / AAA)
Los dispositivos móviles son particularmente propensos a fallos de contraste debido a la iluminación ambiental variable (luz solar directa, deslumbramiento exterior, atenuación de la batería OLED). Busca un contraste de texto WCAG 2.2 AAA ($\ge 7:1$) y cumple estrictamente con WCAG 1.4.11 (contraste de límites no textuales $\ge 3:1$) y WCAG 1.4.1 (uso del color).
7.1 La Trampa del FilterChip en Modo Oscuro de Material 3
- El Defecto: Por defecto, el
FilterChipde Material 3 en modo oscuro usasecondaryContainer(#37474F) para su fondo seleccionado contra lasurfaceoscura (#14181E). Esto produce una relación de contraste inaceptable de 1.83:1. Con poca luz o deslumbramiento, los usuarios no pueden distinguir qué chip está activo. - La Solución:
- Aplica una definición de límite de alto contraste con
selectedBorderColor = MaterialTheme.colorScheme.primary(que produce una relación de contraste de 9.88:1 en modo oscuro). - Establece
selectedBorderWidth = 1.5.dp. - Cumple con WCAG 1.4.1 (estados que no dependen solo del color) proporcionando un icono de marca de verificación inicial:
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 ) )
- Aplica una definición de límite de alto contraste con
7.2 Estándares de Legibilidad con Luz Solar y Deslumbramiento
- Texto Primario vs Superficie: Estandariza el modo oscuro con texto de alto contraste (
onSurface#E1E2E8o#F8FAFCsobre#14181E), logrando $\ge 13:1$ de contraste (superando WCAG AAA). - Contornos de Controles de Formulario:
OutlinedTextFieldy los bordes de botones de opción / casillas de verificación deben usar tokensoutlineque mantengan al menos $3:1$ de contraste contra las tarjetas contenedoras (Color(0xFF8D9199)orgba(255, 255, 255, 0.20)explícito). - Filas de Presets Compactos: Al organizar filas de chips compactos (por ejemplo, presets de retención
1h,4h,12h,1d), preserva los tamaños objetivo y aplicamaxLines = 1conselectedBorderColorpara que los chips nunca se envuelvan o se vean ambiguos.
8. Repositorios de Referencia de Código Abierto
Inspecciona estos proyectos de código abierto líderes en la industria de Android para obtener inspiración en diseño e implementación:
android/nowinandroid: Muestra emblemática de Compose con Material 3 de Google que demuestra tematización dinámica, diseños adaptativos y arquitectura limpia.skydoves/Pokedex-Compose: Transiciones de elementos compartidos de nivel de producción (.sharedBounds/.sharedElement) y gestos suaves de hojas de detalle.plainhub/plain-app: Implementación excepcional de gestión local del teléfono, tematización Material You y sincronización cliente-servidor receptiva.Gurupreet/ComposeCookBook: Catálogo exhaustivo de animaciones de Compose de producción, barras inferiores y micro-interacciones de lienzo personalizadas.signalapp/Signal-Android: Estándar de oro para almacenamiento local cifrado, trabajadores de buzón de salida en segundo plano y resiliencia de red.