Reúne a cada mascota perdida con su hogar — una comunidad geolocalizada en tiempo real.
La pérdida y el abandono siguen siendo un problema real. Hoy todo se difunde por redes sociales, generando información desorganizada y fragmentada.
Instagram, Facebook o WhatsApp: información dispersa, sin orden ni seguimiento.
Alternativas como Welppy dependen por completo de la masa de usuarios activos en la zona.
FindPet o Tractive ofrecen tiempo real, pero exigen hardware adicional de pago.
Cuantos más usuarios activos, más útil es la plataforma.
Publica a su mascota perdida con foto, descripción y ubicación GPS para que la comunidad la localice.
Reporta un avistamiento desde la calle: sube foto y GPS para conectarlo con el dueño.
Rol ADMIN para moderar contenido inapropiado: desestimar reportes o eliminar publicaciones.
Facilitar el reencuentro de animales perdidos con sus dueños.
Publica y consulta animales perdidos en tu radio geográfico, sobre un mapa en tiempo real.
Toda la información del animal — foto, descripción, ubicación, fecha y estado — en un único lugar.
Dueños que buscan y colaboradores que ayudan, unidos por el bienestar animal.
Cualquier persona implicada en el bienestar animal. Dos grandes franjas:
Público joven, nativo digital y familiar con apps y dispositivos móviles.
Adultos implicados en la acción comunitaria, con interés por colaborar.
Seis sprints — seis entregas al cliente — de octubre 2025 a abril 2026.
Lenguaje oficial de Android. Corrutinas, sealed classes, extension functions y null-safety como pilares del proyecto.
Gestiona el hash de contraseñas, el flujo OAuth con Google y emite JWT firmados para cada petición.
PostgreSQL con extensión geoespacial. Row Level Security, Storage para fotos y API REST autogenerada.
Gestión de estado reactiva con Jetpack. La app sobrevive a rotaciones de pantalla sin recargar datos.
Cliente HTTP asíncrono de JetBrains. Apunta directamente a la API REST de Supabase sin SDK intermedio, con interceptor JWT automático.
Mapa interactivo con marcadores personalizados tipo lágrima y clustering automático de publicaciones cercanas.
Glide carga y cachea imágenes desde Storage. Compressor reduce fotos de ~4 MB a ~400 KB antes de subirlas.
Inyección de dependencias sobre Dagger 2. Siete módulos como singletons gestionan el ciclo de vida de todas las dependencias.
Regla de dependencia estricta: las capas internas no conocen a las externas. La lógica queda independiente del framework, la base de datos y la interfaz.
Fragments, ViewModels y layouts XML. El estado de la UI se expone con StateFlow.
Repositorios sobre Firebase Auth y Supabase, accedidos con el cliente HTTP Ktor.
Modelos puros, interfaces de repositorios y casos de uso (Login, Register, Publish). Sin dependencias de Android.
Cuatro niveles: Presentación (Fragments + ViewModels), Dominio (interfaces de repositorios, modelos y casos de uso), Datos (repositorios y data sources) y Servicios externos. Firebase Auth emite el JWT y Supabase persiste los datos vía Ktor.
Seis entidades del modelo de dominio en Kotlin, sin dependencias de Android.
Seis entidades en PostgreSQL estructuran usuarios, animales y sus publicaciones.
Interacción entre capas al publicar un animal perdido o encontrado.
Firebase Auth emite JWT enviados a Supabase vía Ktor; RLS valida el acceso a nivel de base de datos.
ST_DWithin en WGS 84 vía RPC. Marcadores en lágrima con foto circular; agrupación con android-maps-utils.
RPC crea Animal y Publicacion en una sola transacción. Fotos subidas en paralelo con corrutinas Kotlin.
Siete módulos (App, Data, Repository, Auth, Location, Storage, UseCase) como singletons.
StateFlow + repeatOnLifecycle: las colecciones se cancelan al ir a segundo plano, evitando memory leaks.
Eliminación en cascada: Firebase → Supabase → Storage. Confirmación en dos pasos obligatoria.
Casos funcionales en dispositivo real y emulador: validación de formularios, conectividad y compatibilidad.
Un recorrido por el flujo completo: del registro al reencuentro.
Todas las funcionalidades clave, verificadas en dispositivo real y emulador:
Elegido por su robustez y el soporte nativo de inicio de sesión con Google, sin gestionar el hash de contraseñas manualmente.
Base de datos relacional con extensión geoespacial. Coordinar los dos sistemas —especialmente el manejo del token JWT— fue uno de los desafíos más interesantes del proyecto.
Experiencia práctica en Clean Architecture + MVVM, Kotlin con corrutinas, integración de dos backends, PostGIS, Hilt y navegación Jetpack.
Mensajería directa con Supabase Realtime, sin exponer el teléfono públicamente.
Rangos e insignias por actividad para incentivar la colaboración y la retención.
Firebase Cloud Messaging: avisa a usuarios cercanos cuando se publica un animal.
Démosles a las mascotas perdidas una nueva oportunidad de volver a casa.
Un JSON Web Token que Firebase Auth emite cuando el usuario inicia sesión. Identifica al usuario logueado y se refresca automáticamente cada hora.
Resultado: el usuario nunca gestiona el token manualmente — Firebase lo refresca en silencio y Ktor lo adjunta en cada llamada.
Un Flow caliente de Kotlin Coroutines que almacena siempre el último valor emitido y lo entrega inmediatamente a cualquier nuevo colector.
StateFlow es independiente de Android: funciona en el dominio sin importar androidx. Más potente con operadores de Flow (map, filter, combine).
Framework HTTP cliente/servidor de JetBrains escrito en Kotlin puro. Totalmente asíncrono mediante corrutinas, sin callbacks ni threads.
Retrofit usa anotaciones y reflexión en tiempo de compilación. Ktor es más idiomático en Kotlin, con un DSL más expresivo y soporte nativo de corrutinas sin adaptadores adicionales.
Librería de inyección de dependencias de Google para Android, construida sobre Dagger 2. Gestiona el ciclo de vida de las dependencias con anotaciones en tiempo de compilación.
La inyección manual escala mal: con 7 módulos y múltiples capas, Hilt elimina el boilerplate y garantiza que cada dependencia tenga el scope correcto (singleton, viewmodel, fragment…).
Punto de unión: firebase_uid actúa como clave foránea en Supabase, sincronizando ambos sistemas sin duplicar la lógica de autenticación.
Característica nativa de PostgreSQL que aplica políticas de acceso a nivel de fila, dentro de la base de datos.
Extensión de PostgreSQL que añade tipos y funciones geoespaciales. Trabajamos en WGS 84 (el mismo sistema que GPS).
Una pantalla completa de la app. Tiene dos archivos: MapFragment.kt (lógica) y fragment_map.xml (vistas). No sabe nada de Supabase ni Firebase.
El puente entre el XML y el Kotlin. Android genera automáticamente una clase con un atributo por cada vista, sin findViewById. Es type-safe: el compilador verifica que el ID existe.
Gestiona la lógica de la pantalla sin tocar ninguna vista. Habla con los repositorios y expone el resultado via StateFlow. No sabe que existe el Fragment.
El Fragment se destruye al rotar el móvil. Si la petición de red estaba en el Fragment, se cancela y vuelve a empezar. El ViewModel sobrevive a las rotaciones.
viewModelScope: el contenedor de coroutines del ViewModel. Cuando el ViewModel se destruye, cancela automáticamente todas sus coroutines. No hay peticiones huérfanas.
La forma de ejecutar tareas que tardan tiempo (red, BD) sin bloquear el hilo principal (el que dibuja la pantalla). Si lo bloqueas más de 5 segundos, Android mata la app.
Indica que una función puede pausarse mientras espera y reanudarse cuando tiene el resultado, sin ocupar el hilo mientras espera. Solo puede llamarse desde otra función suspend o desde una coroutine.
Si el usuario rota el móvil, el ViewModel sobrevive → viewModelScope sobrevive → las coroutines en curso no se interrumpen.
Un tipo con un número cerrado de subtipos conocidos en tiempo de compilación. El compilador obliga a manejar todos los casos en el when, eliminando los estados no contemplados.
Cada estado puede llevar datos propios. Success lleva la lista de publicaciones; Error lleva el mensaje. Un enum no puede hacer eso.
El when en el Fragment es exhaustivo: si añades un nuevo estado y olvidas manejarlo, el compilador da error.
Define QUÉ se puede hacer, sin decir CÓMO. Es Kotlin puro, sin imports de Android ni Supabase. El ViewModel solo conoce esta interfaz.
Define CÓMO se hace realmente. Llama a Supabase con Ktor, convierte DTOs a modelos con .toDomain().
Le dice a Hilt: "cuando alguien pida PublicationRepository, dale PublicationRepositoryImpl". El ViewModel nunca sabe que existe Supabase.
Modelo de dominio propio (domain/model/GeoPoint.kt). No es de Google Maps ni de PostGIS. Agrupa latitud y longitud con validación en el constructor, evitando confundir el orden de los parámetros.
Calcula la distancia real en km entre dos coordenadas GPS sobre la esfera terrestre. Se usa en cliente para calcular distancias una vez que las publicaciones ya están descargadas del servidor.
ListFragment necesita mezclar estados (activo y resuelto), lo que complica el filtrado en servidor. Haversine en cliente lo resuelve con menor coste de desarrollo.
Se llama en onResume() cada vez que vuelves al mapa. Si los datos tienen más de 60 segundos hace una nueva petición; si son frescos, no hace nada.
Una app de animales perdidos no necesita actualizaciones al milisegundo. Un retraso de hasta 60 segundos es perfectamente aceptable para el caso de uso.
Gestiona la navegación entre Fragments de forma centralizada. Toda la navegación está definida en nav_graph.xml. Una sola MainActivity contiene un NavHostFragment donde se muestran todos los Fragments.
Genera clases tipo-seguras para pasar datos entre pantallas. Si el argumento no existe → error en compilación, no en ejecución. Evita los Bundle con strings mágicos.
Al publicar un animal, limpia el back stack hasta el mapa. El usuario no puede volver al formulario pulsando atrás — evita publicar dos veces por accidente.
Una sola MainActivity con un NavHostFragment. Los Fragments se intercambian dentro de ella. Ventaja: contexto compartido, transiciones fluidas, BottomNavigation gestionada en un solo lugar.
El estado interno es privado. Solo el ViewModel puede escribir en _uiState; los Fragments solo leen uiState (solo lectura).
PublicationRepository define QUÉ se puede hacer, sin decir CÓMO. El ViewModel no sabe si los datos vienen de Supabase, de un mock o de un archivo local.
BasePublishViewModel tiene la lógica común de publicación. PublishLostAnimalViewModel y PublishFoundAnimalViewModel heredan y añaden lo específico de cada flujo.
El ViewModel recibe un PublicationRepository. En producción recibe PublicationRepositoryImpl; en tests recibe un MockPublicationRepository. El ViewModel no nota la diferencia.
Librería para cargar y mostrar imágenes desde URLs de Supabase Storage de forma eficiente. Gestiona automáticamente:
La app no envía WhatsApp directamente. Usa Intents de Android: mensajes al sistema operativo para que él abra la app adecuada.
El usuario elige desde qué app compartir. La app no elige por él.