Ir al contenido principal

Persistencia de Datos Local en Android con Room

Persistencia de Datos Local en Android con Room

Casi todas las aplicaciones Android necesitan almacenar datos localmente, ya sea para funcionar sin conexión, guardar preferencias complejas, o manejar grandes cantidades de información estructurada. Android utiliza SQLite como motor de base de datos subyacente, pero trabajar directamente con las APIs de SQLite puede ser verboso y propenso a errores.

Aquí es donde entra Room, la librería de persistencia recomendada por Google y parte de Android Jetpack. Room actúa como una capa de abstracción sobre SQLite, facilitando enormemente el acceso a la base de datos mientras aprovecha toda la potencia de SQLite.

Si quieres una forma robusta, eficiente y moderna de manejar bases de datos locales en tu app Android, ¡Room es la herramienta que necesitas!


¿Por qué elegir Room?

Room no es solo una capa de abstracción, ofrece ventajas clave:

  • Verificación de Consultas SQL en Tiempo de Compilación: Room valida tus consultas SQL mientras escribes el código. ¡Adiós a los errores de SQL que solo aparecían en tiempo de ejecución!
  • Reducción Drástica de Código Boilerplate: Olvídate de manejar cursores, SQLiteOpenHelper, y convertir manualmente datos entre objetos y filas de base de datos. Room lo hace por ti con anotaciones simples.
  • API Intuitiva y Tipada: Interactúas con tus datos usando objetos Kotlin/Java y métodos claramente definidos en tus DAOs (Data Access Objects).
  • Integración Fluida con Componentes Jetpack: Funciona perfectamente con Coroutines, Flow, LiveData y Paging 3, permitiendo crear UIs reactivas y eficientes que observan los cambios en la base de datos.
  • Facilita las Pruebas: Es más sencillo realizar pruebas unitarias de tu lógica de base de datos.
  • Manejo de Migraciones: Proporciona un sistema claro para manejar cambios en el esquema de la base de datos a medida que tu app evoluciona.

Componentes Clave de Room

Room se basa en tres componentes principales:

  1. Entity (@Entity): Representa una tabla en tu base de datos. Es una clase (usualmente una data class en Kotlin) anotada con @Entity. Sus campos representan las columnas de la tabla. Necesita al menos una clave primaria (@PrimaryKey).
  2. DAO (Data Access Object - @Dao): Una interfaz (o clase abstracta) anotada con @Dao. Aquí defines los métodos para interactuar con la base de datos (insertar, consultar, actualizar, borrar) usando anotaciones como @Insert, @Query, @Update, @Delete. Room genera la implementación automáticamente.
  3. Database (@Database): Una clase abstracta que extiende RoomDatabase y está anotada con @Database. Sirve como el punto de acceso principal a tu base de datos. Aquí listas todas las entidades (tablas) y DAOs asociados a esa base de datos.

Configurando Room en tu Proyecto Android

Veamos cómo configurar Room paso a paso:

Paso 1: Añadir Dependencias de Gradle

Añade las dependencias de Room a tu archivo app/build.gradle.kts. Usaremos KSP (Kotlin Symbol Processing) para el procesamiento de anotaciones.


// build.gradle.kts (Nivel de App)

plugins {
    // ... otros plugins
    id("com.google.devtools.ksp") // Asegúrate de tener KSP
}

android {
    // ...
}

dependencies {
    val room_version = "2.6.1" // Usa la última versión estable de Room

    implementation("androidx.room:room-runtime:$room_version")
    // Opcional: Soporte para Kotlin Extensions y Coroutines
    implementation("androidx.room:room-ktx:$room_version")
    // Procesador de anotaciones (KSP)
    ksp("androidx.room:room-compiler:$room_version")

    // Opcional: si usas KAPT en lugar de KSP
    // annotationProcessor("androidx.room:room-compiler:$room_version")
    // kapt("androidx.room:room-compiler:$room_version")

    // Opcional: Para testing de Room
    // testImplementation("androidx.room:room-testing:$room_version")

    // Opcional: Para soporte de Paging 3 con Room
    // implementation("androidx.room:room-paging:$room_version")

    // ... otras dependencias
}

Nota: Reemplaza 2.6.1 con la última versión estable disponible en la documentación oficial de Room. Sincroniza Gradle.

Paso 2: Definir la Entidad (Entity)

Crea una clase de datos para representar una tabla. Por ejemplo, una tabla de usuarios:


package com.example.myapp.data.local.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users") // Define el nombre de la tabla
data class UserEntity(
    @PrimaryKey(autoGenerate = true) // Clave primaria autoincremental
    val id: Int = 0,

    @ColumnInfo(name = "user_name") // Define el nombre de la columna (opcional si coincide con el nombre del campo)
    val name: String,

    @ColumnInfo(name = "email_address")
    val email: String
)

Paso 3: Definir el DAO (Data Access Object)

Crea una interfaz con métodos anotados para las operaciones de base de datos. Usa suspend para operaciones de escritura/lectura única y Flow para consultas observables.


package com.example.myapp.data.local.dao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapp.data.local.model.UserEntity
import kotlinx.coroutines.flow.Flow // Para consultas observables

@Dao
interface UserDao {

    // Inserta un usuario. Si ya existe (misma primary key), lo reemplaza.
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: UserEntity) // 'suspend' para Coroutine

    // Inserta varios usuarios
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(users: List)

    // Actualiza un usuario existente
    @Update
    suspend fun updateUser(user: UserEntity)

    // Borra un usuario específico
    @Delete
    suspend fun deleteUser(user: UserEntity)

    // Borra todos los usuarios (¡cuidado!)
    @Query("DELETE FROM users")
    suspend fun deleteAllUsers()

    // Obtiene todos los usuarios como una lista (operación única)
    @Query("SELECT * FROM users ORDER BY user_name ASC")
    suspend fun getAllUsersList(): List

    // Obtiene todos los usuarios como un Flow (se actualiza automáticamente si cambian los datos)
    @Query("SELECT * FROM users ORDER BY user_name ASC")
    fun getAllUsersFlow(): Flow>

    // Obtiene un usuario por su ID (operación única)
    @Query("SELECT * FROM users WHERE id = :userId LIMIT 1")
    suspend fun getUserById(userId: Int): UserEntity?

    // Obtiene un usuario por su ID como Flow (se actualiza si cambia)
    @Query("SELECT * FROM users WHERE id = :userId LIMIT 1")
    fun getUserByIdFlow(userId: Int): Flow
}

Paso 4: Definir la Base de Datos (Database)

Crea la clase abstracta que extiende RoomDatabase.


package com.example.myapp.data.local

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.myapp.data.local.dao.UserDao
import com.example.myapp.data.local.model.UserEntity

@Database(
    entities = [UserEntity::class], // Lista todas las entidades de esta BD
    version = 1, // Versión de la BD (incrementar en cambios de esquema y añadir migración)
    exportSchema = false // Opcional: exportar esquema a un fichero (útil para migraciones complejas)
)
abstract class AppDatabase : RoomDatabase() {

    // Declara los DAOs que provee esta base de datos
    abstract fun userDao(): UserDao
    // abstract fun otherDao(): OtherDao // Si tuvieras más DAOs

    companion object {
        // Volatile asegura que la instancia sea siempre visible para todos los hilos
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            // Retorna la instancia si ya existe, si no, la crea de forma segura (synchronized)
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database" // Nombre del fichero de la base de datos
                )
                // .addMigrations(MIGRATION_1_2, ...) // Añadir migraciones aquí si es necesario
                // .fallbackToDestructiveMigration() // Opcional: Borra y recrea la BD si no hay migración (¡pérdida de datos!)
                .build()
                INSTANCE = instance
                // return instance
                instance // Devuelve la instancia creada
            }
        }
    }
}

Usando Room: Realizando Operaciones

Una vez configurado, interactuar con la base de datos es sencillo, especialmente desde un ViewModel usando Coroutines.

1. Obtener la Instancia de la Base de Datos y el DAO:
Normalmente, inyectarías el DAO usando Hilt o Koin, o lo obtendrías desde la instancia singleton de la base de datos.

Ejemplo en un ViewModel (asumiendo que el DAO es inyectado o accesible):


package com.example.myapp.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapp.data.local.dao.UserDao
import com.example.myapp.data.local.model.UserEntity
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

// Asumiendo que UserDao es inyectado (ej. con Hilt o Koin)
// class UserViewModel @Inject constructor(private val userDao: UserDao) : ViewModel() {
class UserViewModel(private val userDao: UserDao) : ViewModel() { // Ejemplo sin inyección DI explícita

    // Exponer el Flow de usuarios como StateFlow para la UI
    val allUsers: StateFlow> = userDao.getAllUsersFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000L), // Mantiene el flow activo 5s después de que no haya suscriptores
            initialValue = emptyList() // Valor inicial mientras se carga
        )

    // Función para añadir un nuevo usuario
    fun addUser(name: String, email: String) {
        viewModelScope.launch { // Usar Coroutine para operaciones suspend
            val newUser = UserEntity(name = name, email = email)
            userDao.insertUser(newUser)
        }
    }

    // Función para borrar un usuario
    fun deleteUser(user: UserEntity) {
        viewModelScope.launch {
            userDao.deleteUser(user)
        }
    }

    // Función para actualizar un usuario
    fun updateUser(user: UserEntity) {
        viewModelScope.launch {
            userDao.updateUser(user)
        }
    }
}

2. Observar Datos en la UI (Ejemplo con Compose):


@Composable
fun UserListScreen(userViewModel: UserViewModel /* Obtenido con hiltViewModel(), viewModel(), etc. */) {
    val users by userViewModel.allUsers.collectAsStateWithLifecycle()

    Column {
        // Botones para añadir usuario (llamarían a userViewModel.addUser(...))

        if (users.isEmpty()) {
            Text("No hay usuarios.")
        } else {
            LazyColumn {
                items(users) { user ->
                    UserItem(user = user, onDeleteClick = { userViewModel.deleteUser(user) })
                }
            }
        }
    }
}

@Composable
fun UserItem(user: UserEntity, onDeleteClick: () -> Unit) {
    Row(/*...*/) {
        Text(text = "${user.name} (${user.email})")
        Button(onClick = onDeleteClick) { Text("Borrar") }
    }
}

Conceptos Avanzados (Brevemente)

  • Migraciones: Cuando cambias el esquema de tu base de datos (añadir/modificar tablas/columnas), necesitas definir `Migration`s para preservar los datos de los usuarios entre versiones de la app.
  • Type Converters (@TypeConverter): Permiten a Room almacenar tipos de datos personalizados que no soporta nativamente (ej. Date, listas simples), convirtiéndolos a tipos que SQLite sí entiende.
  • Relaciones (@Relation): Para definir y consultar relaciones entre entidades (uno a uno, uno a muchos).

Conclusión

Room simplifica enormemente la persistencia de datos en Android. Al ofrecer verificación de SQL en tiempo de compilación, reducir el código boilerplate y integrarse perfectamente con Coroutines y Flow, se convierte en la opción ideal y recomendada por Google para manejar bases de datos SQLite locales.

Si necesitas almacenamiento estructurado en tu aplicación, ¡no dudes en adoptar Room!

Para más detalles sobre migraciones, relaciones y otras funcionalidades, consulta la documentación oficial de Room.

Comentarios

Entradas populares de este blog

Principios SOLID del Tio Bob

  Principios SOLID del Tio Bob Los principios sólidos son un conjunto de pautas de diseño de software que se centran en lograr código limpio, modular y mantenible. Estos principios fueron propuestos por Robert C. Martin (Uncle Bob) y se consideran fundamentales en el desarrollo de software orientado a objetos. Los cinco principios sólidos son los siguientes:   1. Principio de Responsabilidad Única (Single Responsibility Principle, SRP): Una clase debería tener una única responsabilidad. Esto significa que una clase debe tener una única razón para cambiar. Al tener una responsabilidad única, se logra un código más cohesivo y fácil de mantener.   2. Principio de Abierto/Cerrado (Open/Closed Principle, OCP): Las entidades de software (clases, módulos, etc.) deben estar abiertas para su extensión pero cerradas para su modificación. Esto significa que el comportamiento de una entidad puede ser extendido sin necesidad de modificar su código fuente original.  ...

Desarrollador junior C#

Habilidades que se esperan de un desarrollador junior (C#)   1.      Conocimientos básicos de programación: Debes tener una comprensión sólida de los conceptos fundamentales de programación, como variables, estructuras de control, bucles, funciones, etc. 2.      Dominio del lenguaje C#: Debes tener conocimientos sólidos del lenguaje C# y su sintaxis. Debes estar familiarizado con los conceptos orientados a objetos, como clases, herencia, polimorfismo, etc. 3.      Conocimientos de .NET Framework: C# se utiliza principalmente para el desarrollo en el entorno de .NET Framework, por lo que debes tener un conocimiento básico de esta plataforma, incluyendo las bibliotecas y clases comunes que se utilizan en el desarrollo de aplicaciones. 4.      Experiencia con Visual Studio: Visual Studio es el entorno de desarrollo integrado (IDE) más popular para C#. Debes estar familiarizado con su uso y ser capa...

Programación asíncrona comparación Kotlin y C#

La programación asíncrona es un enfoque en la programación que permite que las tareas se ejecuten de manera independiente y no bloqueante. En lugar de esperar a que una tarea se complete antes de pasar a la siguiente, las tareas se pueden ejecutar en paralelo o de manera secuencial, lo que mejora la eficiencia y la capacidad de respuesta de las aplicaciones. ¿Como implementar programación asíncrona en Kotlin? Usando Coroutines. Las coroutines en Kotlin son una forma de escribir código asíncrono de manera más concisa y legible. En lugar de bloquear el hilo principal mientras esperamos a que se complete una tarea, las coroutines permiten que el hilo siga ejecutándose mientras esperamos que una tarea asincrónica termine. Esto mejora la eficiencia y la capacidad de respuesta de las aplicaciones. Un ejemplo básico: KOTLIN import kotlinx.coroutines.* fun main() {     println("Inicio")     // Lanzar una coroutine     GlobalScope.launch {       ...