Persistencia con Room en Android

Resumen

Cuando una app Android se cierra o el sistema operativo la destruye, los datos en memoria desaparecen. Aprende a agregar persistencia de datos con Room en Android para que tus tareas sobrevivan al ciclo de vida de la aplicación, reemplazando un fake local data source por una base de datos real.

¿Por qué reemplazar un fake data source por Room?

Un fake local data source mantiene los datos solo en memoria. Cuando cierras la app o el sistema la destruye, todo vuelve al estado inicial. Room resuelve esto con una capa de abstracción sobre SQLite que persiste la información en disco.

¿Qué es Room en Android? Es la librería oficial de persistencia que envuelve SQLite y te permite trabajar con entidades, DAOs y bases de datos usando anotaciones de Kotlin [00:18].

Cómo activar Room en build.gradle

El primer paso es habilitar las dependencias en los archivos de configuración:

  • Descomenta el alias y la configuración de Room en el build.gradle del módulo.
  • Activa las librerías de Room para que queden disponibles.
  • Descomenta KSP y Room en el build.gradle a nivel de proyecto.
  • Sincroniza Gradle para que reconozca los cambios [00:50].

Una vez sincronizado, ya puedes empezar a crear los componentes principales.

¿Cómo crear una entidad y la base de datos en Room?

Una entidad representa una tabla. La base de datos agrupa entidades y expone los DAOs que harán las consultas. Ambos componentes son obligatorios para que Room funcione.

Cómo modelar un TaskEntity con anotaciones

Dentro del paquete data se crea la clase TaskEntity marcada con @Entity(tableName = "tasks"). Los campos clave del modelado son:

  • id como @PrimaryKey(autoGenerate = false) porque el ID se genera manualmente.
  • title como campo obligatorio y description como opcional.
  • isCompleted mapeado con @ColumnInfo para convertir camelCase a snake_case.
  • date almacenado como Long porque Room no guarda tipos Date sin un converter [02:30].

Para mantener todo simple, se usan funciones de conversión: una en el companion object que recibe un Task y devuelve un TaskEntity (transformando la fecha con ZoneId.systemDefault() y toEpochMilli()), y una función de extensión toTask() que hace el camino inverso usando LocalDateTime.ofInstant() [03:40].

Cómo definir el TaskDao con queries SQLite

El DAO (Data Access Object) es la interfaz que expone las operaciones sobre la tabla. Se marca con @Dao e incluye queries como:

  • SELECT * FROM tasks para obtener todas las tareas.
  • SELECT * FROM tasks WHERE id = :id para buscar una tarea específica.
  • @Upsert para crear o actualizar (combinación de update + insert).
  • Operaciones de borrado por ID y de toda la tabla [05:10].

La clase TodoDatabase extiende de RoomDatabase, se anota con @Database(entities = [TaskEntity::class], version = 1) y expone el DAO. Dentro del companion object se implementa un singleton con una variable volatile para garantizar una única instancia, construida con Room.databaseBuilder() que recibe contexto, clase y nombre [06:30].

¿Cómo conectar Room con los ViewModels usando inyección manual?

La parte interesante llega cuando los ViewModels necesitan dependencias. Como aún no se usa Hilt, hay que cablear todo manualmente, y este paso prepara el terreno para la siguiente clase.

Por qué crear un RoomTaskLocalDataSource

Esta clase implementa la interfaz TaskLocalDataSource y recibe un taskDao y un CoroutineDispatcher. Las operaciones de base de datos no se pueden hacer en el hilo principal, así que cada función usa withContext(Dispatchers.IO) para mover el trabajo al background [08:15].

Las operaciones se homologan una a una respecto al fake: getAllTasks mapea entidades a modelos con flowOn(Dispatchers.IO), mientras que agregar, actualizar y borrar se envuelven en withContext con las transformaciones fromTask y toTask correspondientes.

¿Por qué usar Dispatchers.IO en operaciones de Room? Las consultas a base de datos son operaciones bloqueantes. Ejecutarlas en el hilo principal congela la UI; Dispatchers.IO las mueve a un pool optimizado para entrada y salida.

Cómo configurar TodoApplication y DataSourceFactory

Para compartir el DataSource y el Dispatcher de forma global, se crea una clase TodoApplication que hereda de Application. Dentro expone dos propiedades:

  • dispatcherIO: CoroutineDispatcher con Dispatchers.IO por defecto.
  • dataSource: TaskLocalDataSource obtenido desde un DataSourceFactory.createDataSource(context, dispatcher).

El DataSourceFactory es un object que instancia TodoDatabase.getDatabase(context) y devuelve un RoomTaskLocalDataSource con el DAO y el dispatcher. Después hay que registrar la clase en el AndroidManifest.xml con android:name=".TodoApplication" [11:45].

Dentro de cada ViewModel, el companion object expone una factory tipo viewModelFactory que recibe SavedStateHandle y el TaskLocalDataSource desde la application. En NavigationRoot, el ViewModel se crea pasando HomeScreenViewModel.Factory o TaskViewModelFactory según corresponda.

¿Funciona la persistencia al cerrar la aplicación?

Al ejecutar la app, la lista aparece vacía. Al crear tarea uno marcada como completada y tarea dos como incompleta, ambas se guardan. Al cerrar la app y volver a abrirla, la información persiste.

El cambio es enorme: pasaste de una fuente volátil a una que sobrevive al ciclo de vida del proceso. El costo fue toda la inicialización manual en los ViewModels, factorías y la clase application. ¿Te animas a probarlo en tu propio proyecto antes de migrar a Hilt? Cuéntanos en los comentarios cómo te fue con la conversión de tu fake data source.