Saltar a contenido

119. Codelab Room

Basado en este codelab

REVISAR ESTO

119.1 La aplicación

Vamos a desarrollar la siguiente arquitectura:
Alt text

Usaremos :

  • RoomaDatabase: con las clases de acceso a base de datos: SqlLite, Entities y DAO
  • Repository: abstracción para acceso a datos
  • ViewModel con FlowState en lugar de LiveData
  • UI con compose

119.2 Actualiamos Gradle

Para usar el complemento de procesador de anotaciones kapt necesitamoa añadir:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp") version "1.8.21-1.0.11"
}

y las dependencias

 // room
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    annotationProcessor("androidx.room:room-compiler:2.6.1")
    ksp("androidx.room:room-compiler:2.6.2")

119.3 Crear la entidad

119.4 Crear DAO

DAO

La biblioteca de Room proporciona anotaciones de conveniencia, como @Insert, @Delete y @Update, para definir métodos que realizan inserciones, actualizaciones y eliminaciones simples sin necesidad de escribir una instrucción de SQL.

Operaciones:

  • Insertar o agregar un elemento nuevo
  • Actualizar un elemento existente para actualizar el * nombre, el precio y la cantidad
  • Obtener un elemento específico según su clave primaria, id
  • Obtener todos los elementos para que puedas mostrarlos
  • Borrar una entrada de la base de datos

Para Query

import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Query("SELECT * from usuario WHERE id = :myId")
fun getUsuario(myId: Int): Flow<Usuario>

El IDE verifica que usuario esta declarada como tabla en la entity y que id es un campo de la tabla.
Observar la expresión :myId para indicar el argumento del método.

119.5 Crear una instancia de base de datos

Lo normal es tener una única copia de acceso a la base de datos. Para ello usamos el patrón de diseño singleton

Este es el proceso general para obtener la instancia RoomDatabase:

  • Crea una clase public abstract que extienda RoomDatabase. La nueva clase abstracta que defines actúa como un contenedor de la base de datos. La clase que defines es abstracta porque Room crea la implementación por ti.
  • Anota la clase con @Database. En los argumentos, enumera las entidades para la base de datos y establece el número de versión.
  • Define una propiedad o un método abstracto que muestre una instancia de UserDao, y Room genera la implementación por ti.
  • Solo necesitas una instancia de RoomDatabase para toda la app, así que haz que RoomDatabase sea un singleton.
  • Usa el Room.databaseBuilder de Room para crear tu base de datos (usuario_database), solo si no existe. De lo contrario, muestra la base de datos existente.

Crear el fichero UsuarioDatabase.kt

La anotación @Database requiere varios argumentos para que Room pueda compilar la base de datos.

  1. Especifica el Uusuario como la única clase con la lista de entities.
  2. Establece version como 1. Cada vez que cambies el esquema de la tabla de la base de datos, debes aumentar el número de versión.
  3. Establece exportSchema como false para que no se conserven las copias de seguridad del historial de versiones de esquemas.
@Database(entities = [Item::class], version = 1, exportSchema = false)

Dentro del cuerpo de la clase, declara una función abstracta que muestre el ItemDao de modo que la base de datos sepa sobre el DAO.

abstract fun itemDao(): ItemDao
Debajo de la función abstracta, define un companion object, que permite el acceso a los métodos para crear u obtener la base de datos y usa el nombre de clase como calificador.

companion object {}

Dentro del objeto companion, declara una variable anulable privada Instance para la base de datos y, luego, inicializala en null. La variable Instance conserva una referencia a la base de datos, cuando se crea una. Esto ayuda a mantener una sola instancia de la base de datos abierta en un momento determinado, que es un recurso costoso para crear y mantener.

Anota Instance con @Volatile. El valor de una variable volátil nunca se almacena en caché, y todas las lecturas y escrituras son desde y hacia la memoria principal. Estas funciones ayudan a garantizar que el valor de Instance esté siempre actualizado y sea el mismo para todos los subprocesos de ejecución. Eso significa que los cambios realizados por un subproceso en Instance son visibles de inmediato para todos los demás subprocesos.

@Volatile
private var Instance: InventoryDatabase? = null

Debajo de Instance, mientras estás dentro del objeto companion, define un método getDatabase() con un parámetro Context que necesite el compilador de bases de datos.

Muestra un tipo UsuarioDatabase. Aparecerá un mensaje de error porque getDatabase() aún no muestra nada.

 fun getDatabase(context: Context): UusuarioDatabase 

Es posible que varios subprocesos soliciten una instancia de base de datos al mismo tiempo, lo que genera dos bases de datos en lugar de una. Este problema se conoce como condición de carrera. Unir el código para obtener la base de datos dentro de un bloque synchronized significa que solo un subproceso de ejecución a la vez puede ingresar este bloque de código, lo que garantiza que la base de datos solo se inicialice una vez.

  • Dentro de getDatabase(), muestra la variable Instance o, si Instance es nula, inicialízala dentro de un bloque synchronized{}. Para ello, usa el operador elvis (?:).
  • Pasa this, el objeto complementario. Solucionarás el error en los pasos posteriores.
return Instance ?: synchronized(this) { }

Dentro del bloque synchronized, usa el compilador de bases de datos para obtener una base de datos y pasa a Room.databaseBuilder() el contexto de la aplicación, la clase de la base de datos y un nombre para la base de datos, usuario_database.

Después de build(), agrega un bloque also y asigna Instance = it para mantener una referencia a la instancia de base de datos recién creada.

Room.databaseBuilder(context, UsuarioDatabase::class.java, "usuario_database")
    .build()
    .also { Instance = it }

NOTA Kotlin: also es una función de extensión standar de librería que se ejecuta sobre el objeto (referenciado con it dentro del bloque ). Normalmente se utiliza para efectos secundarios sin modificar el objeto. (similar pero para otras funciones: let apply , run, with)

Ejemplo completo:

/**
* Clase Database con patron singleton
*/
@Database(entities = [Usuario::class], version = 1, exportSchema = false)
abstract class UsuarioDatabase : RoomDatabase() {

    abstract fun usuarioDAO(): UsuarioDao  // Room creara la instancia

    // el patrón de diseño singleton
    companion object {
        @Volatile
        private var Instance: UsuarioDatabase? = null

        fun getDatabase(context: Context): UsuarioDatabase {
            // Si la instancia no es nula, la devuelve, en otro caso crea la instancia de la base de datos.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, UsuarioDatabase::class.java, "usuario_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

119.6 Crear el Repository

En esta tarea, implementarás la interfaz UsuarioRepository y la clase OfflineUsuarioRepository para proporcionar entidades get, insert, delete y update de la base de datos.

Abre el archivo UsuarioRepository.kt bajo el paquete data.

Agrega las siguientes funciones a la interfaz, que se asignan a la implementación de DAO.

119.7 AppContainer

/**
 * App container for Dependency injection.
 */
interface AppContainer {
    val usuarioRepository: UsuarioRepository
}

/**
 * [AppContainer] implementation that provides instance of [OfflineItemsRepository]
 */
class AppDataContainer(private val context: Context) : AppContainer {
    /**
     * Implementation for [ItemsRepository]
     */
    override val usuarioRepository: UsuarioRepository by lazy {
        OfflineUsuariosRepository(UsuarioDatabase.getDatabase(context).usuarioDao ())
    }
}

119.8 Conexión con IU ViewModel