Trabajo Fin de Grado · 2º DAM
Come Back

Reúne a cada mascota perdida con su hogar — una comunidad geolocalizada en tiempo real.

Equipo
Gabriel Cruz · Diego Sánchez · Cosmin Stancu
Centro · Curso
CPIFP Los Enlaces · DAM · 2025 / 2026
Índice

Qué vamos a ver

01IntroducciónProblema · solución · objetivos
02OrganizaciónMetodología · 6 sprints
2.1Diseño / ArquitecturaStack · Clean Architecture
2.2DiagramasArquitectura · clases · E-R · Secuencia
2.3Implementación y pruebasStack · testing
03Vídeo de la appDemo en funcionamiento
04ConclusionesResultados · futuro
Come Back
02 / 17
01 · Introducción — El problema

Cada año se pierden miles de mascotas.
La información, no.

La pérdida y el abandono siguen siendo un problema real. Hoy todo se difunde por redes sociales, generando información desorganizada y fragmentada.

Redes sociales

Instagram, Facebook o WhatsApp: información dispersa, sin orden ni seguimiento.

Apps comunitarias

Alternativas como Welppy dependen por completo de la masa de usuarios activos en la zona.

Collares GPS

FindPet o Tractive ofrecen tiempo real, pero exigen hardware adicional de pago.

No existe una plataforma móvil dedicada que lo unifique todo
Come Back
03 / 17
01 · Introducción — La solución

Una app que conecta a dos perfiles.

Cuantos más usuarios activos, más útil es la plataforma.

Dueño

Publica a su mascota perdida con foto, descripción y ubicación GPS para que la comunidad la localice.

Colaborador ciudadano

Reporta un avistamiento desde la calle: sube foto y GPS para conectarlo con el dueño.

Panel de administración

Rol ADMIN para moderar contenido inapropiado: desestimar reportes o eliminar publicaciones.

Pantalla de mapa
Come Back
04 / 17
01 · Introducción — Objetivos y destinatarios

El objetivo

Facilitar el reencuentro de animales perdidos con sus dueños.

Geolocalización

Publica y consulta animales perdidos en tu radio geográfico, sobre un mapa en tiempo real.

Base de datos centralizada

Toda la información del animal — foto, descripción, ubicación, fecha y estado — en un único lugar.

Comunidad solidaria

Dueños que buscan y colaboradores que ayudan, unidos por el bienestar animal.

Destinatarios

Cualquier persona implicada en el bienestar animal. Dos grandes franjas:

18–34AÑOS

Público joven, nativo digital y familiar con apps y dispositivos móviles.

35–60AÑOS

Adultos implicados en la acción comunitaria, con interés por colaborar.

Come Back
05 / 17
02 · Organización — Metodología

Metodología ágil, ciclo de vida incremental

Seis sprints — seis entregas al cliente — de octubre 2025 a abril 2026.

S1
7–14 oct
Idea
Elección, contexto y recogida de requisitos.
S2
15–21 oct
Acuerdo
Tareas, planificación y metodología.
S3
22 oct–11 nov
Análisis y diseño
Diagramas, mockups y tecnologías.
S4
12–30 nov
Desarrollo
Sistema completo y manual de usuario.
S5
1 dic–20 mar
Implementación
Desarrollo, pruebas e implantación.
S6
21 mar–25 abr
Cierre
Resultados, ajustes y entrega.
Come Back
06 / 17
2.1 · Diseño / Arquitectura

Stack tecnológico

Kotlin

Lenguaje oficial de Android. Corrutinas, sealed classes, extension functions y null-safety como pilares del proyecto.

Firebase Auth

Gestiona el hash de contraseñas, el flujo OAuth con Google y emite JWT firmados para cada petición.

Supabase + PostGIS

PostgreSQL con extensión geoespacial. Row Level Security, Storage para fotos y API REST autogenerada.

StateFlow + ViewModel

Gestión de estado reactiva con Jetpack. La app sobrevive a rotaciones de pantalla sin recargar datos.

Ktor HTTP Client

Cliente HTTP asíncrono de JetBrains. Apunta directamente a la API REST de Supabase sin SDK intermedio, con interceptor JWT automático.

Google Maps SDK + android-maps-utils

Mapa interactivo con marcadores personalizados tipo lágrima y clustering automático de publicaciones cercanas.

Glide + Compressor

Glide carga y cachea imágenes desde Storage. Compressor reduce fotos de ~4 MB a ~400 KB antes de subirlas.

Hilt

Inyección de dependencias sobre Dagger 2. Siete módulos como singletons gestionan el ciclo de vida de todas las dependencias.

Come Back
07 / 17
2.1 · Diseño / Arquitectura

Clean Architecture por capas

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.

Kotlin 1.9MVVMCoroutinesStateFlowHilt
ui/

Presentación

Fragments, ViewModels y layouts XML. El estado de la UI se expone con StateFlow.

data/

Datos

Repositorios sobre Firebase Auth y Supabase, accedidos con el cliente HTTP Ktor.

domain/

Dominio

Modelos puros, interfaces de repositorios y casos de uso (Login, Register, Publish). Sin dependencias de Android.

Come Back
08 / 17
2.2 · Diagramas — Arquitectura
Diagrama de arquitectura de Come Back

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.

Firebase AuthSupabase + PostGISKtorHilt
Come Back
09 / 17
2.2 · Diagramas — Estructura de clases
Diagrama de clases

Estructura de clases

Seis entidades del modelo de dominio en Kotlin, sin dependencias de Android.

Usuarioperfil del usuario
Animalentidad base del animal
Publicacionavistamiento o pérdida
FotoPublicacionimágenes asociadas
UbicacionGPScoordenadas geográficas
Reportemoderación comunitaria
Come Back
10 / 17
2.2 · Diagramas — Entidad–Relación
Diagrama Entidad-Relación

Modelo de datos

Seis entidades en PostgreSQL estructuran usuarios, animales y sus publicaciones.

usersperfil del usuario
animalsentidad base del animal
publicationsavistamiento o pérdida
publication_photosimágenes asociadas
reportsmoderación comunitaria
admin_logsacciones de administración
PostGISGEOGRAPHY(POINT, 4326)Row Level Security
Come Back
11 / 17
2.2 · Diagramas — Secuencia
Diagrama de secuencia

Flujo de publicación

Interacción entre capas al publicar un animal perdido o encontrado.

1
El ViewModel recibe la acción del usuario y valida el formulario.
2
Invoca PublishUseCase, que coordina el guardado de Animal y Publicacion.
3
El Repositorio ejecuta la transacción atómica en Supabase vía Ktor.
4
El resultado fluye de vuelta al UI como un StateFlow.
PublishUseCasecreate_animal_and_publication
Come Back
12 / 17
2.3 · Implementación y pruebas

De la arquitectura al producto

Autenticación dual

Firebase Auth emite JWT enviados a Supabase vía Ktor; RLS valida el acceso a nivel de base de datos.

Mapa PostGIS + clustering

ST_DWithin en WGS 84 vía RPC. Marcadores en lágrima con foto circular; agrupación con android-maps-utils.

Publicación atómica

RPC crea Animal y Publicacion en una sola transacción. Fotos subidas en paralelo con corrutinas Kotlin.

Inyección con Hilt

Siete módulos (App, Data, Repository, Auth, Location, Storage, UseCase) como singletons.

Estado reactivo

StateFlow + repeatOnLifecycle: las colecciones se cancelan al ir a segundo plano, evitando memory leaks.

Derecho al olvido (RGPD)

Eliminación en cascada: Firebase → Supabase → Storage. Confirmación en dos pasos obligatoria.

Pruebas

Casos funcionales en dispositivo real y emulador: validación de formularios, conectividad y compatibilidad.

Come Back
13 / 17
03 · Vídeo de la app

La app en acción

Un recorrido por el flujo completo: del registro al reencuentro.

Inicio de sesión y registro
Filtrar perdidos/encontrados por zona
Buscador de zona y detalles
Calibrar ubicación y zoom del mapa
Reportar mascota perdida / avistamiento
Agrupación/desagrupación de reportes
Editar perfil e invitar usuarios
Soporte y gestión de reportes
Demo captura 1
Demo captura 2
Come Back
14 / 17
04 · Conclusiones — Objetivos cumplidos

Plazos cumplidos, esenciales implementados

Todas las funcionalidades clave, verificadas en dispositivo real y emulador:

Autenticación dual
Publicación geolocalizada
Mapa con clustering
Lista con filtros
Detección de coincidencias
Ciclo de vida de casos
Perfil con estadísticas
Reportes + panel admin
RGPD · derecho al olvido
LOPD · edad mínima 14

Decisiones técnicas destacadas

Firebase Auth

Elegido por su robustez y el soporte nativo de inicio de sesión con Google, sin gestionar el hash de contraseñas manualmente.

Supabase + PostGIS

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.

Come Back
15 / 17
04 · Conclusiones — Lo que viene

Conocimientos adquiridos y próximos pasos

Experiencia práctica en Clean Architecture + MVVM, Kotlin con corrutinas, integración de dos backends, PostGIS, Hilt y navegación Jetpack.

Chat en tiempo real

Mensajería directa con Supabase Realtime, sin exponer el teléfono públicamente.

Gamificación

Rangos e insignias por actividad para incentivar la colaboración y la retención.

Notificaciones push

Firebase Cloud Messaging: avisa a usuarios cercanos cuando se publica un animal.

Come Back
16 / 17
Come Back · TFG 2025/2026
Gracias

Démosles a las mascotas perdidas una nueva oportunidad de volver a casa.

Gabriel Cruz · Diego Sánchez · Cosmin Stancu
Q&A · Preguntas del tribunal

¿Cómo manejamos el JWT de Firebase?

¿Qué es?

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.

¿Dónde se usa?

1
Ktor intercepta cada petición HTTP a Supabase.
2
Lee el JWT actual de Firebase (refresco transparente).
3
Lo inyecta en Authorization: Bearer <jwt>.
4
Supabase verifica el firebase_uid y aplica las RLS.
Firebase AuthKtor HttpSendRow Level Security

Interceptor en DataModule.kt

client.plugin(HttpSend).intercept { req ->
  val user = firebaseAuth.currentUser
  if (user != null) {
    user.getIdToken(false)
      .addOnSuccessListener {
        req.headers["Authorization"] =
          "Bearer ${it.token}"
      }
  }
}

Resultado: el usuario nunca gestiona el token manualmente — Firebase lo refresca en silencio y Ktor lo adjunta en cada llamada.

Come Back
Q&A · A0
Q&A · Preguntas del tribunal

¿Qué es StateFlow y cómo lo usamos?

¿Qué es?

Un Flow caliente de Kotlin Coroutines que almacena siempre el último valor emitido y lo entrega inmediatamente a cualquier nuevo colector.

¿Por qué no LiveData?

StateFlow es independiente de Android: funciona en el dominio sin importar androidx. Más potente con operadores de Flow (map, filter, combine).

Cómo lo usamos

1
Cada ViewModel expone un StateFlow<UiState> de solo lectura.
2
Internamente usa un MutableStateFlow que solo el ViewModel modifica.
3
Los Fragments colectan con repeatOnLifecycle(STARTED): se pausa en background y evita memory leaks.
Kotlin CoroutinesHot FlowMVVM
Come Back
Q&A · A1
Q&A · Preguntas del tribunal

¿Qué es Ktor y cómo lo usamos?

¿Qué es?

Framework HTTP cliente/servidor de JetBrains escrito en Kotlin puro. Totalmente asíncrono mediante corrutinas, sin callbacks ni threads.

¿Por qué Ktor y no Retrofit?

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.

Cómo lo usamos

1
Cliente HTTP para comunicarse con la API REST de Supabase.
2
Plugin Auth interceptor: añade el JWT de Firebase en cada petición automáticamente.
3
Serialización JSON con kotlinx.serialization sin reflexión en runtime.
JetBrainsCoroutineskotlinx.serialization
Come Back
Q&A · A2
Q&A · Preguntas del tribunal

¿Qué es Hilt y cómo lo usamos?

¿Qué es?

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.

¿Por qué Hilt y no manual?

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…).

Nuestros 7 módulos

AppModulecontexto y configuración global
AuthModuleFirebaseAuth + cliente Ktor
DataModulefuentes de datos remotas
RepositoryModuleimplementaciones de repositorios
LocationModuleFusedLocationProvider
StorageModuleSupabase Storage
UseCaseModulecasos de uso del dominio
Come Back
Q&A · A3
Q&A · Preguntas del tribunal

¿Por qué Firebase y Supabase?

Firebase Auth

Estándar de facto para auth en Android.
OAuth con Google en 3 líneas de código.
Emite JWT firmados que Supabase verifica.
SDK robusto con manejo de sesión y refresco automático de tokens.

Supabase

PostgreSQL real: consultas SQL complejas y PostGIS para geodatos.
Row Level Security: seguridad a nivel de fila en BD, no solo en app.
Storage integrado para fotos, con políticas de acceso propias.
API REST autogenerada desde el esquema, sin escribir backend.

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.

Come Back
Q&A · A4
Q&A · Preguntas del tribunal

¿Por qué Clean Architecture?

1
Separación de responsabilidades. Cada capa solo conoce a la inmediatamente inferior, nunca al revés. El dominio no sabe nada de Android.
2
Testabilidad. Los casos de uso y modelos se pueden testear en JUnit puro, sin emulador ni mocks de Android.
3
Independencia de frameworks. Si mañana cambiamos Supabase por otro backend, solo tocamos la capa de Datos. El dominio y la UI no cambian.
4
Escalabilidad. Añadir nuevas funcionalidades (chat, notificaciones) implica añadir nuevos casos de uso, sin romper los existentes.
Presentación
Fragments + ViewModels + StateFlow
↓ solo llama a ↓
Dominio
UseCases + Interfaces + Modelos
↓ solo llama a ↓
Datos
Repos + DataSources + Ktor
Come Back
Q&A · A5
Q&A · Preguntas del tribunal

RLS y PostGIS: seguridad y geodatos

Row Level Security (RLS)

Característica nativa de PostgreSQL que aplica políticas de acceso a nivel de fila, dentro de la base de datos.

Cada tabla tiene políticas que verifican el JWT.
Solo puedes leer y editar tus propios datos.
La seguridad funciona aunque alguien obtenga la API key.
Rol ADMIN con acceso extendido definido en BD.

PostGIS

Extensión de PostgreSQL que añade tipos y funciones geoespaciales. Trabajamos en WGS 84 (el mismo sistema que GPS).

GEOGRAPHY(POINT, 4326): coordenadas reales en metros.
ST_DWithin: filtra publicaciones dentro de un radio.
RPC get_publications_nearby: una sola llamada devuelve resultados paginados y ordenados por distancia.
Come Back
Q&A · A6
Q&A · Preguntas del tribunal

Cumplimiento RGPD y LOPD

Derecho al olvido (Art. 17 RGPD)

1
Confirmación en dos pasos antes de eliminar la cuenta.
2
Firebase Auth → Supabase → Storage
3
Cascada elimina publicaciones, fotos y todos los datos asociados en una sola operación.
4
Si falla cualquier paso, se revierte y se notifica al usuario.

Otras medidas

Edad mínima 14 años (LOPD-GDD): checkbox obligatorio en el registro.
Consentimiento explícito al registrarse para el tratamiento de datos de ubicación.
Datos mínimos: solo recogemos lo necesario para la funcionalidad (no hay tracking ni analytics).
RLS: ningún usuario accede a datos de otro, garantizado a nivel de base de datos.
Come Back
Q&A · A7
Q&A · Preguntas del tribunal

Fragments y ViewBinding

¿Qué es un Fragment?

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.

¿Qué es ViewBinding?

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.

Patrón _binding — ¿por qué dos variables?

1
_binding es var y nullable → permite asignarla a null.
2
binding es solo lectura → si la usas cuando es null, explota inmediatamente.
3
En onDestroyView() se hace _binding = null para evitar el memory leak: el Fragment puede sobrevivir a su Vista en el back stack.
Come Back
Q&A · A8
Q&A · Preguntas del tribunal

¿Qué es un ViewModel?

¿Qué hace?

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.

¿Por qué no poner la lógica en 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.

Fragment
Pinta vistas
Captura clicks
No sobrevive rotaciones
Sin coroutines propias
ViewModel
Lógica y red
Sin referencias a vistas
Sobrevive rotaciones
viewModelScope

viewModelScope: el contenedor de coroutines del ViewModel. Cuando el ViewModel se destruye, cancela automáticamente todas sus coroutines. No hay peticiones huérfanas.

Come Back
Q&A · A9
Q&A · Preguntas del tribunal

¿Qué son las Coroutines?

¿Qué son?

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.

La palabra clave suspend

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.

Relación con StateFlow y ViewBinding

Coroutine hace el trabajo (petición de red)
StateFlow transporta el resultado al Fragment
ViewBinding pinta el resultado en las vistas

Si el usuario rota el móvil, el ViewModel sobrevive → viewModelScope sobrevive → las coroutines en curso no se interrumpen.

Come Back
Q&A · A10
Q&A · Preguntas del tribunal

Sealed Classes — estados de la UI

¿Qué son las Sealed Classes?

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.

¿Por qué no un enum o un Boolean?

Cada estado puede llevar datos propios. Success lleva la lista de publicaciones; Error lleva el mensaje. Un enum no puede hacer eso.

MapState en la app

Initialpantalla recién abierta
Loadingpetición en curso → spinner
Success(publications)pinta marcadores
Error(message)muestra el error

El when en el Fragment es exhaustivo: si añades un nuevo estado y olvidas manejarlo, el compilador da error.

Come Back
Q&A · A11
Q&A · Preguntas del tribunal

Repositorios: Dominio vs Datos

Dominio — la interfaz (el contrato)

Define QUÉ se puede hacer, sin decir CÓMO. Es Kotlin puro, sin imports de Android ni Supabase. El ViewModel solo conoce esta interfaz.

interface PublicationRepository {
  suspend fun getPublicationsNearby(): Result<List<Publication>>
}

Datos — la implementación

Define CÓMO se hace realmente. Llama a Supabase con Ktor, convierte DTOs a modelos con .toDomain().

class PublicationRepositoryImpl : PublicationRepository {
  // aquí llama a Supabase con PostGIS
}

RepositoryModule — el conector

Le dice a Hilt: "cuando alguien pida PublicationRepository, dale PublicationRepositoryImpl". El ViewModel nunca sabe que existe Supabase.

Come Back
Q&A · A12
Q&A · Preguntas del tribunal

GeoPoint, Haversine y estrategias geográficas

¿Qué es GeoPoint?

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.

Fórmula Haversine

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.

Dos estrategias de búsqueda geográfica

MapFragment
PostGIS en servidor
Solo ACTIVO · más eficiente
ListFragment
Haversine en cliente
ACTIVO + RESUELTO

ListFragment necesita mezclar estados (activo y resuelto), lo que complica el filtrado en servidor. Haversine en cliente lo resuelve con menor coste de desarrollo.

Come Back
Q&A · A13
Q&A · Preguntas del tribunal

¿Por qué refreshIfStale y no WebSockets?

¿Qué hace refreshIfStale?

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.

Es una decisión consciente

Una app de animales perdidos no necesita actualizaciones al milisegundo. Un retraso de hasta 60 segundos es perfectamente aceptable para el caso de uso.

WebSockets
Instantáneo
Consume batería constantemente
Conexión permanente por usuario
Coste de infraestructura alto
refreshIfStale
~
Hasta 60s de retraso
Batería mínima
HTTP normal
Coste bajo
Come Back
Q&A · A14
Q&A · Preguntas del tribunal

Navigation Component

¿Qué es?

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.

SafeArgs

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.

popUpTo + popUpToInclusive

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.

Single Activity

Una sola MainActivity con un NavHostFragment. Los Fragments se intercambian dentro de ella. Ventaja: contexto compartido, transiciones fluidas, BottomNavigation gestionada en un solo lugar.

Come Back
Q&A · A15
Q&A · Preguntas del tribunal

Los 4 pilares de la POO en la app

Encapsulación

El estado interno es privado. Solo el ViewModel puede escribir en _uiState; los Fragments solo leen uiState (solo lectura).

Abstracción

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.

Herencia

BasePublishViewModel tiene la lógica común de publicación. PublishLostAnimalViewModel y PublishFoundAnimalViewModel heredan y añaden lo específico de cada flujo.

Polimorfismo

El ViewModel recibe un PublicationRepository. En producción recibe PublicationRepositoryImpl; en tests recibe un MockPublicationRepository. El ViewModel no nota la diferencia.

Come Back
Q&A · A16
Q&A · Preguntas del tribunal

Glide e Intents

Glide — carga de imágenes

Librería para cargar y mostrar imágenes desde URLs de Supabase Storage de forma eficiente. Gestiona automáticamente:

Descarga en segundo plano con coroutines
Caché en memoria y en disco
Redimensiona al tamaño del ImageView
Cancela la descarga si el Fragment se destruye

Intents — llamadas y compartir

La app no envía WhatsApp directamente. Usa Intents de Android: mensajes al sistema operativo para que él abra la app adecuada.

ACTION_DIALabre la app de teléfono con el número precargado
ACTION_SENDabre el selector de apps (WhatsApp, Telegram, email…)

El usuario elige desde qué app compartir. La app no elige por él.

Come Back
Q&A · A17