119. Codelab Room¶
Basado en este codelab
REVISAR ESTO
119.1 La aplicación¶
Vamos a desarrollar la siguiente arquitectura:
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¶
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
, yRoom
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.
- Especifica el
Uusuario
como la única clase con la lista de entities. - 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.
- 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
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 ())
}
}