Tu gestor personal de series para Android
Episodeo es una aplicación Android nativa para llevar el seguimiento de tus series de televisión. Diseñada bajo principios de Clean Architecture y MVVM, ofrece una solución robusta para entusiastas de las series que buscan una experiencia premium, optimizada y ad-free.
Autor: Ismail Haddouche Rhali
- Busca cualquier serie usando la base de datos de TMDB (más de 800.000 títulos)
- Organiza tus series por estado: Viendo, Pendiente, Terminada o Abandonada
- Puntúa las series que has terminado con una nota del 1 al 10
- Crea listas personalizadas para agrupar series como quieras
- Comparte tus listas con un código único y sigue las listas de otras personas
- Tus datos se sincronizan entre dispositivos gracias a Firebase
- Funciona offline: la información ya vista se guarda localmente con Room
- Tema oscuro y claro configurable
| Capa | Tecnología |
|---|---|
| Lenguaje | Kotlin Coroutines + Flow |
| UI Framework | Jetpack Compose + Material Design 3 |
| Arquitectura | MVVM + Repository Pattern + Clean Architecture |
| Dependency Injection | Hilt (Dagger) |
| Cloud Storage | Firebase Firestore (Real-time Sync) |
| Auth | Firebase Auth (Google OAuth 2.0) |
| Local Persistence | Room (SQLite abstraction) |
| Networking | Retrofit 2 + Moshi (JSON Parsing) |
| Image Loading | Coil (Compose optimized) |
| Quality Assurance | JUnit 4, Compose Testing |
| Automation | GitHub Actions (CI/CD) |
La app sigue el patrón MVVM recomendado por Google para Android moderno.
┌─────────────────────────────────────────────────────┐
│ Capa de Vista (UI) │
│ Jetpack Compose — Screens + Components │
│ LoginScreen · HomeScreen · MyListsScreen · ... │
└────────────────────────┬────────────────────────────┘
│ observa StateFlow / State
┌────────────────────────▼────────────────────────────┐
│ ViewModel │
│ HomeViewModel (Hilt) │
│ Estado de UI · Lógica de negocio · Coroutines │
└────────────────────────┬────────────────────────────┘
│ llama a
┌────────────────────────▼────────────────────────────┐
│ Repository │
│ SeriesRepository (Singleton) │
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ Firebase │ │ Room (Cache Local) │ │
│ │ Firestore │ │ SeriesDao │ │
│ │ Auth │ │ AppDatabase │ │
│ └─────────────────┘ └──────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ TMDB API (Retrofit) │ │
│ │ TmdbApiService · TmdbApi │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
El Repository implementa una estrategia Network-First con fallback local:
- Online: consulta Firestore / TMDB → guarda el resultado en Room → devuelve los datos.
- Offline: la red falla → devuelve los datos de Room (caché).
En el caso de los datos de usuario, Cloud Prevails: Firestore sobreescribe Room en cada sincronización.
app/src/main/java/com/haddouche/episodeo/
│
├── components/ Componentes Compose reutilizables
│ ├── AddToListDialog Diálogo para añadir serie a una lista
│ ├── FollowListDialog Diálogo para seguir lista por código
│ ├── HomeTopAppBar Barra superior con búsqueda
│ ├── SavedSeriesItem Ítem de serie en lista
│ ├── SearchBar Barra de búsqueda
│ ├── SearchResultItem Ítem de resultado de búsqueda
│ ├── SeriesCarousel Carrusel horizontal de series
│ ├── SeriesDetailDialog Dialog de detalle completo de serie
│ └── SideMenu Menú lateral de navegación
│
├── data/local/ Capa de persistencia local (Room)
│ ├── AppDatabase Definición de la base de datos Room
│ ├── Converters Type converters para Room
│ ├── SeriesDao DAO con todas las consultas
│ └── entities/Entities Entidades de Room
│
├── di/
│ └── DatabaseModule Módulo Hilt para proveer el DAO
│
├── models/ Modelos de datos
│ ├── AppList Clase sellada: SystemList + CustomList
│ ├── FollowedList Referencia a lista seguida
│ ├── ShareCode Código de compartición de lista
│ ├── User Datos del usuario autenticado
│ ├── UserList Lista personalizada del usuario
│ ├── UserSeries Estado + puntuación de una serie
│ └── tmdb/ Modelos de respuesta de la API TMDB
│ ├── TmdbCredits Créditos (reparto y equipo)
│ ├── TmdbSearchResponse Respuesta de búsqueda paginada
│ ├── TmdbSeries Serie con todos sus campos
│ └── WatchProvidersResponse Plataformas de streaming
│
├── navigation/ Sistema de navegación
│ ├── AppNavigation Grafo de navegación raíz
│ ├── AppRoutes Rutas definidas como sealed class
│ └── InternalNavigation Navegación interna del MainScreen
│
├── network/
│ └── TmdbApiService Interfaz Retrofit + singleton TmdbApi
│
├── repository/
│ └── SeriesRepository Único punto de acceso a datos
│
├── screens/ Pantallas de la app
│ ├── LoginScreen Pantalla de inicio de sesión
│ ├── MainScreen Contenedor principal con navegación
│ ├── HomeScreen Dashboard con carruseles por estado
│ ├── MyListsScreen Mis listas personalizadas
│ ├── FollowedListsScreen Listas que estoy siguiendo
│ ├── ListDetailScreen Detalle de una lista concreta
│ ├── StatusScreen Series filtradas por estado
│ └── SettingsScreen Tema, gestión de cuenta
│
├── ui/theme/ Tema de Material Design 3
│ ├── Color Paleta de colores
│ ├── Theme Configuración del tema claro/oscuro
│ └── Type Tipografía
│
├── viewmodels/
│ └── HomeViewModel ViewModel principal (Hilt)
│
├── EpisodeoApp Application class con @HiltAndroidApp
└── MainActivity Activity principal con @AndroidEntryPoint
usuarios/{uid}
│ ├── isDarkTheme: Boolean
│ │
│ ├── series/{tmdbId}
│ │ ├── seriesStatus: String ("watching" | "pending" | "completed" | "dropped")
│ │ └── seriesRating: Int? (1-10, opcional en cualquier estado)
│ │
│ ├── mis_listas/{listId}
│ │ ├── id: String
│ │ ├── name: String
│ │ ├── ownerId: String
│ │ ├── isPublic: Boolean
│ │ └── seriesIds: List<Int>
│ │
│ └── listas_seguidas/{listId}
│ ├── listId: String
│ ├── ownerId: String
│ └── listName: String
codigos_compartir/{code}
├── code: String
├── ownerId: String
└── listId: String
Cache local (Room):
| Tabla | Campos | Propósito |
|---|---|---|
user_series |
seriesId, seriesStatus, seriesRating, userId | Estado offline del usuario |
user_lists |
id, name, ownerId, isPublic, seriesIds, userId | Listas offline |
followed_lists |
listId, ownerId, listName, userId | Referencias a listas seguidas |
cached_series |
id, name, posterPath, synopsis, releaseDate | Metadatos TMDB cacheados |
LoginScreen
└── MainScreen (contenedor)
├── HomeScreen /home
├── MyListsScreen /mis_listas
├── FollowedListsScreen /listas_seguidas
├── StatusScreen /estado/{statusName}
├── ListDetailScreen /lista/{listName}
└── SettingsScreen /ajustes
- Android Studio Hedgehog o superior
- JDK 11
- Android SDK:
compileSdk 36,targetSdk 34,minSdk 24 - Cuenta de Firebase
- API key de TMDB (gratuita)
git clone https://github.com/ismailhaddouche/episodeo.git
cd episodeo- Crear un proyecto en Firebase Console
- Añadir una app Android con el paquete
com.haddouche.episodeo - Descargar el archivo
google-services.jsony colocarlo enapp/ - Habilitar en Firebase:
- Authentication → Proveedor Google
- Firestore Database (modo producción o prueba)
- Añadir el SHA-1 de tu keystore de debug en la configuración de la app de Firebase:
./gradlew signingReport
- Registrarse en themoviedb.org y obtener una API key gratuita
- Añadir la clave al archivo
local.propertiesen la raíz del proyecto:CLAVE_API_TMDB=tu_clave_aqui
local.propertiesestá en.gitignore— nunca se sube al repositorio.
Abre el proyecto en Android Studio, sincroniza Gradle y ejecuta sobre un emulador o dispositivo físico (Android 7.0+).
./gradlew test./gradlew test
# Abre el reporte:
start app/build/reports/tests/testDebugUnitTest/index.html./gradlew testDebugUnitTest jacocoTestReport
# Abre el reporte:
start app/build/reports/jacoco/testDebugUnitTest/html/index.html# Solo modelos
./gradlew test --tests "com.haddouche.episodeo.models.*"
# Solo repository
./gradlew test --tests "com.haddouche.episodeo.repository.*"
# Solo ViewModel
./gradlew test --tests "com.haddouche.episodeo.viewmodels.*"Actualmente, el enfoque principal de las pruebas recae sobre los modelos puros y utilidades (debido a limitaciones técnicas recientes con el entorno de testing y Firebase).
| Paquete | Cobertura |
|---|---|
models |
100% |
# Debug (para pruebas)
./gradlew assembleDebug
# APK en: app/build/outputs/apk/debug/app-debug.apk
# Release (firmado)
./gradlew assembleReleasePara firmar el APK de release, configurar el keystore en app/build.gradle.kts.
- Notificaciones Push — avisar cuando se estrena una nueva temporada
- Soporte Kotlin Multiplatform — versión iOS con la misma lógica
- Funcionalidad social — compartir listas públicas con enlace directo
- Modo offline completo — guardar contenido completo de listas seguidas localmente
Este proyecto está publicado bajo la licencia MIT.
| Documento | Descripción |
|---|---|
| docs/SETUP.md | Instalación del APK + configuración del entorno de desarrollo |
| docs/USER_GUIDE.md | Guía de usuario completa con capturas de pantalla |
| docs/TESTING.md | Suite de pruebas, comandos y cobertura |
| CHANGELOG.md | Historial de cambios |
- TMDB — API de series y películas
- Firebase — Autenticación y base de datos en la nube
- Jetpack Compose — UI toolkit moderno de Android























