Casi todas las apps que hago para clientes tienen algo en común: la gente las usa en lugares donde el internet va y viene. Si tu app depende de la red para mostrar cualquier cosa, el usuario ve pantallas vacías o cargando sin parar. La solución que uso desde hace tiempo es guardar los datos en el teléfono con Room y mostrar siempre lo que hay en la base local.
En esta entrada te explico cómo empezar con Room en un proyecto con Kotlin y Jetpack Compose, paso a paso y sin complicarte.
Qué es Room y por qué usarlo
Room es la librería oficial de Android Jetpack para trabajar con SQLite. Por debajo sigue siendo SQLite, pero te ahorra mucho código repetitivo y revisa tus consultas al compilar. Si escribes mal el nombre de una columna, te enteras antes de ejecutar la app, no cuando un usuario abre la pantalla.
Room tiene tres piezas principales:
- Entity: una clase que representa una tabla.
- DAO: una interfaz con las consultas que vas a usar.
- Database: la clase que une todo y te da acceso a los DAO.
Paso 1: agrega las dependencias
Room usa KSP para generar código. En el build.gradle.kts del módulo app agrega el plugin de KSP y estas dependencias. Revisa en la documentación oficial cuál es la versión más reciente, porque cambia seguido.
plugins {
id("com.google.devtools.ksp")
}
dependencies {
val roomVersion = "2.x.x" // usa la versión estable más reciente
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}
Paso 2: define tu entidad
Pongamos un ejemplo sencillo: una lista de notas. Cada nota tiene un id, un título y la fecha en que se creó.
@Entity(tableName = "notas")
data class Nota(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val titulo: String,
val creadaEn: Long = System.currentTimeMillis()
)
Paso 3: escribe el DAO
Aquí está lo más útil de Room para Compose. Si una consulta devuelve un Flow, Room te avisa cada vez que cambian los datos de esa tabla. Tu pantalla se actualiza sola, sin que tengas que volver a pedir nada.
@Dao
interface NotaDao {
@Query("SELECT * FROM notas ORDER BY creadaEn DESC")
fun observarTodas(): Flow<List<Nota>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun guardar(nota: Nota)
@Delete
suspend fun borrar(nota: Nota)
}
Fíjate que las funciones que escriben son suspend. Room no te deja hacer operaciones de base de datos en el hilo principal, y eso es bueno: evita que la app se congele.
Paso 4: crea la base de datos
@Database(entities = [Nota::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun notaDao(): NotaDao
}
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app.db"
).build()
Crea una sola instancia para toda la app. Si usas Hilt, lo normal es proveerla como @Singleton en un módulo. Si no usas inyección de dependencias, guárdala en tu clase Application.
Paso 5: conéctalo con el ViewModel y Compose
Si leíste mi entrada sobre StateFlow en Compose, esto te va a resultar familiar. Convertimos el Flow de Room en un StateFlow que la pantalla observa.
class NotasViewModel(private val dao: NotaDao) : ViewModel() {
val notas: StateFlow<List<Nota>> = dao.observarTodas()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun agregar(titulo: String) {
viewModelScope.launch { dao.guardar(Nota(titulo = titulo)) }
}
}
@Composable
fun NotasScreen(viewModel: NotasViewModel) {
val notas by viewModel.notas.collectAsStateWithLifecycle()
LazyColumn {
items(notas, key = { it.id }) { nota ->
Text(nota.titulo)
}
}
}
Cuando llamas a agregar, Room guarda la nota, el Flow emite la lista nueva y la pantalla se redibuja. No hay que refrescar nada a mano.
La idea offline first en pocas palabras
Con Room ya tienes la base para que tu app funcione sin conexión. El patrón que sigo es simple:
- La pantalla siempre lee de Room.
- Cuando hay red, descargas los datos del servidor y los guardas en Room.
- Room avisa del cambio y la pantalla se actualiza sola.
Así el usuario ve algo útil desde el primer segundo, aunque esté sin señal. Si tu backend es un WordPress, puedes combinarlo con lo que te conté en la entrada sobre la API REST de WordPress.
Errores comunes al empezar
- Cambiar una entidad sin subir la versión. Si agregas una columna, tienes que subir
versiony escribir una migración. Si no, la app falla al abrir en teléfonos que ya tenían la base vieja. - Usar fallbackToDestructiveMigration en producción. Es cómodo mientras desarrollas, pero borra los datos del usuario cuando cambia la versión.
- Crear varias instancias de la base. Usa una sola para toda la app.
- Guardar todo en una sola tabla. Piensa tus tablas como lo harías en cualquier base de datos relacional.
Conclusión
Room es de esas librerías que una vez que aprendes ya no dejas de usar. Te da una base de datos local segura, consultas revisadas al compilar y, con Flow, pantallas que se actualizan solas. Empieza con una tabla sencilla como la de este ejemplo y, cuando te sientas cómodo, agrega la sincronización con tu servidor.
Si quieres aprender Android con Kotlin desde cero, en mis cursos voy paso a paso por estos temas. Y si tienes dudas, escríbeme por WhatsApp o en redes como davidgbdev.



