1. Caso de Estudio: Alarma por Ubicación (Geofencing Alarm)
Proyecto: Sistema de Alarma Geo-referenciada de Alta Fiabilidad
Stack: Flutter (UI) + Kotlin/Swift (Native Core), SQLite, Serverless (AWS Lambda)
Duración: 14 semanas
Equipo: 1 Mobile Engineer + 1 Backend/DevOps
1.1 📋 Resumen Ejecutivo
Este caso de estudio documenta el desarrollo de una aplicación móvil crítica donde el fallo no es una opción. La app despierta al usuario cuando llega a su destino, un problema aparentemente simple que esconde una enorme complejidad técnica debido a las restricciones de los sistemas operativos móviles modernos (Doze mode, matar procesos en background).
1.1.1 Tecnologías Principales
- Mobile Frontend: Flutter (para UI multiplataforma rápida y expresiva).
- Mobile Core: Kotlin (Android Foreground Services) y Swift (iOS Background Tasks) para la lógica crítica.
- Arquitectura: Hexagonal / Clean Architecture (independencia entre UI y Core).
- Persistencia: SQLite (Drift) con enfoque Offline-First.
- Backend: AWS Serverless (Lambda + DynamoDB) para sincronización de favoritos (costo casi cero).
1.1.2 Métricas de Éxito
- ✅ North Star Metric: Frecuencia de Viajes Monitoreados con Éxito (> 98%).
- ✅ Eficiencia de Batería: < 5% de consumo por hora de monitoreo activo.
- ✅ Latencia de Activación: < 30 segundos desde cruzar el geofence.
- ✅ Customer Effort Score (CES): < 2.0 (Muy bajo esfuerzo para configurar una alarma).
1.2 🎯 1. PRD (Product Requirements Document)
El modelo de negocio se basa en LTV > CAC con un enfoque Freemium (Ads no intrusivos + Donaciones).
1.2.1 1.1 Contexto y Job To Be Done (JTBD)
Cuando viajo en transporte público y estoy cansado,
Lo contrato para que me despierte automáticamente al llegar a mi destino,
Para así poder dormir tranquilo sin miedo a pasarme de parada.
1.2.2 1.2 Requisitos Funcionales
| ID | Requisito | Prioridad | Complejidad |
|---|---|---|---|
| RF-01 | Monitoreo en 2do Plano: La app debe monitorear la ubicación incluso si está cerrada o el teléfono bloqueado. | 🔴 Crítica | Muy Alta |
| RF-02 | Alarma Crítica: El sonido debe sonar incluso si el teléfono está en modo "No Molestar" o Silencio. | 🔴 Crítica | Alta |
| RF-03 | Gestión de Ubicaciones: CRUD de alarmas favoritas con nombre, radio y configuración. | 🟠 Alta | Media |
| RF-04 | Búsqueda de Lugares: Integración con Google Places / Mapbox para buscar destinos. | 🟠 Alta | Media |
| RF-05 | Historial de Viajes: Registro de alarmas disparadas exitosamente y fallidas. | 🟢 Media | Baja |
| RF-06 | Modo Ahorro: Desactivar GPS automáticamente si el usuario no se mueve por 15 min. | 🟢 Media | Alta |
| RF-07 | Sincronización Cloud: Respaldo de favoritos en la nube (Login opcional). | 🟢 Baja | Media |
1.2.3 1.3 Requisitos No Funcionales
| ID | Requisito | Métrica / Target | Criticidad |
|---|---|---|---|
| RNF-01 | Consumo de Batería | < 5% por hora de viaje | 🔴 Crítica (Retention Killer) |
| RNF-02 | Latencia de Disparo | < 30s tras cruzar el perímetro | 🔴 Crítica |
| RNF-03 | Tasa de Falsos Positivos | < 1% (Despertar antes de tiempo) | 🟠 Alta |
| RNF-04 | Reinicio Automático | < 5s tras reinicio del SO | 🔴 Crítica |
| RNF-05 | Tiempo de Inicio (TTI) | < 1.5s para ver el mapa | 🟢 Media |
| RNF-06 | Disponibilidad Offline | 100% (Funciona sin internet) | 🔴 Crítica |
1.2.4 1.4 Requisitos de Seguridad y Privacidad
| Requisito | Justificación | Implementación |
|---|---|---|
| Permisos de Ubicación | Datos sensibles (GDPR/CCPA). El usuario debe confiar en la app. | Solicitar "Solo al usar" primero, luego escalar a "Todo el tiempo" con explicación clara (UI nativa). |
| Minimización de Datos | No rastrear al usuario cuando no hay alarma activa. | El servicio de ubicación se DESTRUYE completamente al finalizar la alarma. |
| Anonimización Cloud | Si usa sync, no vincular lugares con identidad real. | IDs aleatorios en DynamoDB. Auth con "Sign in with Apple/Google" (ocultar email). |
1.3 🏗️ 2. Decisiones de Arquitectura
1.3.1 2.1 ¿Por qué Flutter + Nativo (Híbrido Optimizado)?
Esta fue la decisión técnica más importante del proyecto. Analizamos 3 opciones:
| Criterio | PWA | React Native / Flutter (Puro) | Híbrido Optimizado (Flutter UI + Core Nativo) |
|---|---|---|---|
| Desarrollo UI | ✅ Muy Rápido | ✅ Muy Rápido | ✅ Muy Rápido |
| Performance UI | ⚠️ DOM lento | 🟢 60fps | 🟢 120fps (Skia/Impeller) |
| Background Execution | 🔴 No soportado (iOS) | ⚠️ Limitado / Inestable | 🟢 Nativo (Foreground Service / BG Tasks) |
| Acceso a Hardware | ⚠️ API Web limitada | 🟠 Plugins de terceros | 🟢 API Directa (Kotlin/Swift) |
| Riesgo de "Kill" por OS | 🔴 Muy Alto | 🟠 Medio | 🟢 Bajo (Prioridad Alta) |
Decisión: Usar Flutter solo para la capa de presentación (UI) y comunicación. Escribir toda la lógica de la "Máquina de Estados de Alarma" en Kotlin (Android) y Swift (iOS).
Justificación: Las librerías de geofencing de la comunidad open-source suelen ser genéricas y fallan en casos borde (Doze mode agresivo en Samsung/Xiaomi). Al escribir el core nativo, tenemos control total sobre los WakeLocks y las notificaciones de alta prioridad.
1.3.2 2.2 ¿Por qué SQLite (Offline-First)?
Decisión: Usar SQLite (vía Drift en Flutter) como Source of Truth.
Justificación:
- Fiabilidad: La alarma debe funcionar en el subte, en la ruta o en modo avión. Una base de datos local garantiza esto.
- Performance: Consultas instantáneas sin latencia de red.
- Persistencia de Estado: Si el teléfono se reinicia, el servicio nativo consulta SQLite para saber si había una alarma activa y la restaura automáticamente (
BOOT_COMPLETED).
1.4 ⚙️ 3. Core: La Máquina de Estados Finita (FSM)
Para manejar la complejidad asíncrona del GPS y el estado del usuario, implementamos una FSM robusta.
1.4.1 Diagrama de Estados
| Estado | Evento (Trigger) | Transición | Lógica de Negocio |
|---|---|---|---|
| IDLE | Usuario activa alarma | → PENDING | Registrar Lat/Long destino. Validar permisos. Iniciar Servicio. |
| PENDING | distance < 5 * umbral |
→ APPROACHING | Aumentar muestreo GPS (de 5min a 30s). WakeLock parcial. |
| APPROACHING | distance <= umbral |
→ ALERT_ACTIVE | Disparar Alarma Crítica. Romper modo "No Molestar" (AudioAttributes.USAGE_ALARM). |
| ALERT_ACTIVE | Botón "Stop" / Salir de zona | → RESOLVED | Detener sonido. Guardar historial. Mostrar Ad. |
| SNOOZED | Timer acaba | → APPROACHING | Reiniciar verificación de distancia. |
1.4.2 Implementación del Guard (Kotlin/Pseudo-código)
El FSM utiliza Guards para evitar falsos positivos por "rebote" del GPS (Multipath error).
// Android Foreground Service Logic
fun onLocationUpdate(location: Location) {
val distance = calculateDistance(target, location)
when (currentState) {
State.PENDING -> {
// Guard: Solo pasar a APPROACHING si estamos consistentemente cerca
if (distance < approachThreshold) {
consecutiveNearReadings++
// Debounce: Esperar 3 lecturas seguidas para confirmar
if (consecutiveNearReadings >= 3) transitionTo(State.APPROACHING)
} else {
consecutiveNearReadings = 0
}
}
State.APPROACHING -> {
// Adaptive Polling: Más cerca = Más frecuencia (Ahorro batería)
locationRequest.interval = calculateDynamicInterval(distance)
if (distance <= triggerThreshold) {
transitionTo(State.ALERT_ACTIVE)
}
}
// ...
}
}
1.5 🛡️ 4. Resiliencia, Calidad y Batería
Este sistema es análogo a un soporte vital: si falla, el usuario sufre una consecuencia real (perderse).
1.5.1 4.1 Estrategia de Consumo de Batería (FinOps Aplicado)
Usamos Muestreo Adaptativo (Adaptive Polling).
- Lejos (> 10km): Usar Geofencing del OS (despierta la app solo al entrar en la región grande). Consumo casi nulo.
- Cerca (< 5km): Muestreo GPS cada 2-5 minutos.
- Muy Cerca (< 1km): Muestreo GPS cada 30 segundos (Alta precisión).
1.5.2 4.2 Fallback Circuit Breaker
Si el GPS no responde en 2 minutos (túnel, error de hardware):
- Fallback: Intentar obtener ubicación por Red Celular/Wi-Fi (Menor precisión, mayor disponibilidad).
- Degradación Graciosa: Si falla la ubicación, usar el acelerómetro para detectar movimiento y estimar distancia (Dead Reckoning simple) o alertar al usuario con vibración preventiva y notificación "Señal GPS perdida".
1.6 🎨 5. Experiencia de Usuario (UI/UX)
La UI fue diseñada siguiendo principios de "Calm Technology".
1.6.1 Principios Aplicados
- Don't Make Me Think:
- Problema: El usuario no sabe cuántos metros poner de radio.
- Solución: "Smart Radius". La app sugiere el radio basado en la velocidad de acercamiento (ej. si vas en tren rápido, radio de 2km; si caminas, 200m).
- Feedback Visceral:
- Al activar la alarma, una animación de "onda" expansiva confirma visualmente que el monitoreo comenzó.
- Haptic Feedback (vibración) acompaña las transiciones de estado.
- Accesibilidad:
- Modo Oscuro Real: (OLED Black) automático para ahorro de batería nocturno.
- TTS (Text-to-Speech): "Estás llegando a Casa". Vital para usuarios con discapacidad visual o auriculares puestos.
1.7 🧪 6. Estrategia de Testing
Debido a la naturaleza híbrida y de background, el testing fue complejo.
| Tipo de Test | Cobertura | Herramientas | Qué se prueba |
|---|---|---|---|
| Unitarios | 100% Core Logic | flutter_test, JUnit, XCTest |
Lógica de FSM, conversores de unidades, cálculo de distancias. |
| Widget Tests | 90% UI | flutter_test |
Flujos de navegación, validación de formularios, renderizado de listas. |
| Integration | Críticos | patrol / integration_test |
Comunicación Flutter ↔ Nativo (Method Channels). |
| E2E & Stress | Key Flows | Device Farm (Firebase Test Lab) | Simulación de Viaje: Inyectar mock locations GPX para simular un recorrido completo y verificar disparo. |
1.8 📄 7. Documentación y ADRs (Architecture Decision Records)
1.8.1 ADR-001: Lógica de Ubicación Nativa
- Estado: Aceptado
- Contexto: Necesitamos garantizar la ejecución en background en Android 14+ y iOS 17+. Flutter no tiene control fino sobre el ciclo de vida del Service de Android.
- Decisión: Implementar
LocationManageren Kotlin yCLLocationManageren Swift, exponiendo métodos víaMethodChannel. - Consecuencias: Aumenta la complejidad del codebase y requiere conocimientos de 3 lenguajes. Mejora drásticamente la fiabilidad (Metric: Crash-free sessions > 99.9%).
1.8.2 ADR-002: Persistencia Local (SQLite)
- Estado: Aceptado
- Contexto: La app debe funcionar sin internet.
- Decisión: Usar SQLite con la librería
Drift. - Consecuencias: Requiere migraciones de esquema manuales. Garantiza funcionamiento offline 100%.
1.8.3 ADR-003: Renderizado de Mapas
- Estado: Aceptado
- Contexto: Mostrar mapas consume muchos recursos y datos.
- Decisión: Usar
flutter_map(OpenStreetMap) en lugar de Google Maps SDK. - Justificación: Reducción de costos (Google Maps es caro a escala) y mayor flexibilidad de cacheo offline de tiles.
1.9 🔮 8. Lecciones Aprendidas
- No confíes en el emulador: El comportamiento del GPS y Doze mode en emuladores no tiene nada que ver con dispositivos reales (especialmente marcas chinas con capas de optimización agresivas).
- El usuario miente sobre los permisos: Siempre verificar el estado del permiso en
onResume, no asumir que lo tenemos porque lo pedimos ayer. - Audio Focus es difícil: Interrumpir Spotify/Podcast para sonar la alarma requiere gestionar el "Audio Focus" nativo correctamente, bajando el volumen de la música (ducking) en lugar de pausarla abruptamente.