1. Antes de comenzar
La mayoría de las apps de calidad de producción tienen datos que deben guardarse. Por ejemplo, una app podría almacenar una lista de reproducción de canciones, elementos de una lista de tareas pendientes, registros de gastos e ingresos, un catálogo de constelaciones o un historial de datos personales. En la mayoría de estos casos, se usa una base de datos para almacenar esos datos persistentes.
Room es una biblioteca de persistencias que forma parte de Android Jetpack. Es una capa de abstracción que se ubica sobre una base de datos SQLite. SQLite usa un lenguaje especializado (SQL) para realizar operaciones de bases de datos. En lugar de usar SQLite directamente, Room simplifica las tareas de configuración de la base de datos, así como las interacciones con la app. Room también proporciona verificaciones en tiempo de compilación de las instrucciones de SQLite.
Una capa de abstracción es un conjunto de funciones que ocultan la implementación o la complejidad subyacente. Proporciona una interfaz para un conjunto existente de funciones, como SQLite en este caso.
En la siguiente imagen, se puede apreciar el modo en que Room, como fuente de datos, se adapta a la arquitectura general recomendada en este curso. Room es una fuente de datos.
Requisitos previos
- Saber compilar una interfaz de usuario (IU) básica de una app para Android con Jetpack Compose
- Saber usar elementos componibles como
Text
,Icon
,IconButton
yLazyColumn
- Saber usar el elemento componible
NavHost
para definir rutas y pantallas en tu app - Saber navegar entre pantallas con un
NavHostController
- Conocer el componente de la arquitectura de Android
ViewModel
y saber usarViewModelProvider.Factory
para crear una instancia de ViewModels - Conocer los conceptos básicos de simultaneidad
- Saber usar corrutinas para tareas de larga duración
- Conocimientos básicos sobre las bases de datos SQLite y el lenguaje SQL
Qué aprenderás
- Cómo crear la base de datos SQLite y cómo interactuar con ella mediante la biblioteca Room
- Cómo crear una entidad, un objeto de acceso a datos (DAO) y clases de bases de datos
- Cómo usar un DAO para asignar funciones de Kotlin a consultas en SQL
Qué compilarás
- Compilarás una app de Inventory que guarde elementos de inventario en la base de datos SQLite.
Requisitos
- Código de inicio de la app de Inventory
- Una computadora con Android Studio
- Un dispositivo o un emulador con nivel de API 26 o posterior
2. Descripción general de la app
En este codelab, trabajarás con un código de partida de la app de Inventory y le agregarás la capa de la base de datos con la biblioteca de Room. La versión final de la app mostrará una lista de elementos de la base de datos de inventario. El usuario tendrá opciones para agregar un elemento nuevo, actualizar uno existente y borrarlo de la base de datos de inventario. En este codelab, guardarás los datos del elemento en la base de datos de Room. Completarás el resto de la funcionalidad de la app en el siguiente codelab.
3. Descripción general de la app de inicio
Descarga el código de partida para este codelab
Para comenzar, descarga el código de partida:
Como alternativa, puedes clonar el repositorio de GitHub para el código:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout starter
Puedes explorar el código en el repositorio de GitHub de Inventory app
.
Descripción general del código de partida
- Abre el proyecto con el código de partida en Android Studio.
- Ejecuta la app en un dispositivo Android o en un emulador. Asegúrate de que el emulador o dispositivo conectado ejecute un nivel de API 26 o uno superior. El Inspector de bases de datos funciona en emuladores y dispositivos que ejecutan el nivel de API 26 o uno posterior.
- Observa que la app no muestra datos de inventario.
- Presiona el botón de acción flotante (BAF), que te permite agregar elementos nuevos a la base de datos.
La app navega a una pantalla nueva en la que puedes ingresar los detalles del elemento nuevo.
Problemas con el código de partida
- En la pantalla Add Item, ingresa los detalles de un elemento, como el nombre, el precio y la cantidad.
- Presiona Guardar. La pantalla Add Item no se cierra, pero puedes navegar hacia atrás con la tecla de volver. La función de guardar no está implementada, por lo que no se guardan los detalles del elemento.
Ten en cuenta que la app está incompleta y no se implementa la funcionalidad del botón Save.
En este codelab, agregarás el código que usa Room para guardar los detalles del inventario en la base de datos SQLite. Usas la biblioteca de persistencias Room para interactuar con la base de datos SQLite.
Explicación del código
El código de partida que descargaste tiene diseños de pantalla prediseñados. En esta ruta de aprendizaje, te enfocarás en implementar la lógica de la base de datos. La siguiente sección es una breve explicación de algunos de los archivos para comenzar.
ui/home/HomeScreen.kt
Este archivo es la pantalla principal o la primera pantalla de la app, que contiene los elementos componibles para mostrar la lista de inventario. Tiene un BAF para agregar elementos nuevos a la lista. Mostrarás los elementos de la lista más adelante en la ruta de aprendizaje.
ui/item/ItemEntryScreen.kt
Esta pantalla es similar a ItemEditScreen.kt
. Ambos tienen campos de texto para los detalles del elemento. Esta pantalla se muestra cuando se presiona el BAF en la pantalla principal. ItemEntryViewModel.kt
es el ViewModel
correspondiente de esta pantalla.
ui/navigation/InventoryNavGraph.kt
Este archivo es el gráfico de navegación de toda la aplicación.
4. Componentes principales de Room
Kotlin ofrece una manera fácil de trabajar con datos a través de clases de datos. Si bien es fácil trabajar con datos en la memoria mediante clases de datos, cuando se trata de datos persistentes, debes convertirlos en un formato compatible con el almacenamiento de bases de datos. De este modo, necesitas tablas para almacenar los datos y consultas para acceder a ellos y modificarlos.
Los siguientes tres componentes de Room facilitan estos flujos de trabajo.
- Las entidades de Room representan tablas de la base de datos de tu app. Se usan para actualizar los datos almacenados en filas de las tablas y crear filas nuevas para insertarlas.
- Los DAO de Room proporcionan métodos que tu app usa para recuperar, actualizar, insertar y borrar datos en la base de datos.
- La clase de Database de Room es la clase de base de datos que proporciona a tu app instancias de los DAO asociados con esa base de datos.
Más adelante en este codelab, implementarás estos componentes y aprenderás más sobre ellos. En el siguiente diagrama, se muestra cómo los componentes de Room funcionan en conjunto para interactuar con la base de datos.
Agrega dependencias de Room
En esta tarea, agregarás las bibliotecas de componentes de Room necesarias a tus archivos Gradle.
- Abre el archivo de Gradle de nivel de módulo
build.gradle.kts (Module: InventoryApp.app)
. - En el bloque
dependencies
, agrega las dependencias para la biblioteca Room que se muestra en el siguiente código.
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
KSP es una API simple y potente para analizar anotaciones de Kotlin.
5. Crea un elemento Entity
Una clase Entity define una tabla, y cada instancia de esta clase representa una fila en la tabla de la base de datos. Asimismo, tiene asignaciones para indicarle a Room cómo pretende presentar la información en la base de datos e interactuar con ella. En tu app, la entidad conserva información sobre los elementos del inventario, como el nombre, el precio y la cantidad disponible.
La anotación @Entity
marca una clase como una clase Entity de base de datos. Para cada clase Entity, la app crea una tabla de base de datos que contenga los elementos. Cada campo de Entity se representa como una columna en la base de datos, a menos que se indique lo contrario (consulta la documentación sobre Entity para obtener más información). Cada instancia de entidad que se almacena en la base de datos debe tener una clave primaria. La clave primaria se usa para identificar de manera única cada registro o entrada en las tablas de tu base de datos. Una vez que la app asigna una clave primaria, no se puede modificar. Representa el objeto de la entidad, siempre que exista en la base de datos.
En esta tarea, crearás una clase Entity y definirás campos para almacenar la siguiente información de inventario para cada elemento: Int
para almacenar la clave primaria, String
para almacenar el nombre del elemento, double
para almacenar el precio del elemento y Int
para almacenar la cantidad en stock.
- Abre el código de partida en Android Studio.
- Abre el paquete
data
en el paquete basecom.example.inventory
. - Dentro del paquete
data
, abre la clase de KotlinItem
, que representa una entidad de base de datos en tu app.
// No need to copy over, this is part of the starter code
class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
Clases de datos
Las clases de datos se usan principalmente para conservar datos en Kotlin. Se definen con la palabra clave data
. Los objetos de clase de datos de Kotlin tienen algunos beneficios adicionales. Por ejemplo, el compilador genera automáticamente utilidades para comparar, imprimir y copiar elementos como toString()
, copy()
y equals()
.
Ejemplo:
// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}
Para garantizar la coherencia y el comportamiento significativo del código generado, las clases de datos deben cumplir con los siguientes requisitos:
- El constructor principal debe tener al menos un parámetro.
- Todos los parámetros del constructor principal deben ser
val
ovar
. - Las clases de datos no pueden ser
abstract
,open
nisealed
.
Para obtener más información sobre las clases de datos, consulta la documentación correspondiente.
- Prefija la definición de la clase
Item
con la palabra clavedata
para convertirla en una clase de datos.
data class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
- Sobre la declaración de clase
Item
, anota la clase de datos con@Entity
. Usa el argumentotableName
para estableceritems
como el nombre de la tabla de SQLite.
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
- Anota la propiedad
id
con@PrimaryKey
para queid
sea la clave primaria. Una clave primaria es un ID para identificar de manera única cada registro o entrada en la tablaItem
.
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
- Asigna a
id
un valor predeterminado de0
, que es necesario para queid
genere automáticamente valores deid
. - Agrega el parámetro
autoGenerate
a la anotación@PrimaryKey
para especificar si la columna de clave primaria se debe generar de forma automática. SiautoGenerate
está configurado comotrue
, Room generará automáticamente un valor único para la columna de clave primaria cuando se inserte una nueva instancia de entidad en la base de datos. Esto garantiza que cada instancia de la entidad tenga un identificador único, sin tener que asignar valores manualmente a la columna de clave primaria.
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
// ...
)
¡Muy bien! Ahora que creaste una clase Entity, puedes crear un objeto de acceso a datos (DAO) para acceder a la base de datos.
6. Crea el elemento DAO
El objeto de acceso a datos (DAO) es un patrón que puedes usar para separar la capa de persistencia del resto de la aplicación proporcionando una interfaz abstracta. Este aislamiento sigue el principio de responsabilidad única, que viste en los codelabs anteriores.
La funcionalidad del DAO es ocultar todas las complejidades relacionadas con la realización de operaciones de la base de datos en la capa de persistencia, aparte del resto de la aplicación. Esto te permite cambiar la capa de datos independientemente del código que usa los datos.
En esta tarea, definirás un DAO para Room. Los DAO son los componentes principales de Room que son responsables de definir la interfaz que accede a la base de datos.
El DAO que creas es una interfaz personalizada que proporciona métodos convenientes para consultar/recuperar, insertar, borrar y actualizar la base de datos. Room genera una implementación de esta clase en el tiempo de compilación.
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.
Si necesitas definir operaciones más complejas para la inserción, actualización o eliminación, o si necesitas consultar los datos en la base de datos, usa una anotación @Query
.
Como beneficio adicional, a medida que escribes tus consultas en Android Studio, el compilador comprueba si las consultas de SQL tienen errores de sintaxis.
En el caso de la app de Inventory, debes poder hacer lo siguiente:
- 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
Completa los siguientes pasos para implementar el elemento DAO en tu app:
- En el paquete
data
, crea la interfaz de KotlinItemDao.kt
.
- Anota la interfaz
ItemDao
con@Dao
.
import androidx.room.Dao
@Dao
interface ItemDao {
}
- Dentro del cuerpo de la interfaz, agrega una anotación
@Insert
. - Debajo de
@Insert
, agrega una funcióninsert()
que tome una instancia delitem
de la claseEntity
como su argumento. - Marca la función con la palabra clave
suspend
para permitir que se ejecute en un subproceso separado.
Las operaciones de la base de datos pueden demorar mucho tiempo en ejecutarse, por lo que deben hacerlo en un subproceso independiente. Room no permite el acceso a la base de datos en el subproceso principal.
import androidx.room.Insert
@Insert
suspend fun insert(item: Item)
Cuando se insertan elementos en la base de datos, se pueden generar conflictos. Por ejemplo, varios lugares en el código intentan actualizar la entidad con valores diferentes, en conflicto, como la misma clave primaria. Una entidad es una fila en DB. En la app de Inventory, solo insertamos la entidad desde un lugar que es la pantalla Agregar elemento, por lo que no esperamos que haya ningún conflicto y podemos establecer la estrategia de conflicto como Ignorar.
- Agrega un argumento
onConflict
y asígnale un valor deOnConflictStrategy.
IGNORE
.
El argumento onConflict
le indica a Room qué hacer en caso de conflicto. La estrategia OnConflictStrategy.
IGNORE
ignora un elemento nuevo.
Para obtener más información sobre las estrategias de conflicto disponibles, consulta la documentación de OnConflictStrategy
.
import androidx.room.OnConflictStrategy
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
Ahora, Room
genera todo el código necesario para insertar item
en la base de datos. Cuando llamas a cualquiera de las funciones DAO que están marcadas con anotaciones de Room, Room ejecuta la consulta en SQL correspondiente en la base de datos. Por ejemplo, cuando llamas al método anterior, insert()
desde tu código Kotlin, Room
ejecuta una consulta en SQL para insertar la entidad en la base de datos.
- Agrega una función nueva con la anotación
@Update
que tome unItem
como parámetro.
La entidad que se actualiza tiene la misma clave primaria que la que se pasa. Puedes actualizar algunas o todas las demás propiedades de la entidad.
- Al igual que con el método
insert()
, marca esta función con la palabra clavesuspend
.
import androidx.room.Update
@Update
suspend fun update(item: Item)
Agrega otra función con la anotación @Delete
para borrar elementos y convertirla en una función de suspensión.
import androidx.room.Delete
@Delete
suspend fun delete(item: Item)
No hay ninguna anotación de conveniencia para la funcionalidad restante, por lo que debes usar la anotación @Query
y proporcionar consultas de SQLite.
- Escribe una consulta de SQLite para recuperar un elemento específico de la tabla de elementos según el
id
especificado. El siguiente código proporciona una consulta de muestra que selecciona todas las columnas deitems
, dondeid
coincide con un valor específico yid
es un identificador único.
Ejemplo:
// Example, no need to copy over
SELECT * from items WHERE id = 1
- Agrega una anotación
@Query
. - Usa la consulta de SQLite del paso anterior como un parámetro de cadena a la anotación
@Query
. - Agrega un parámetro
String
a@Query
, que es una consulta de SQLite para recuperar un elemento de la tabla correspondiente.
La consulta ahora indica que se seleccionen todas las columnas de items
, donde id
coincide con el argumento :id
. Observa que :id
usa la notación de dos puntos en la consulta para hacer referencia a argumentos en la función.
@Query("SELECT * from items WHERE id = :id")
- Después de la anotación
@Query
, agrega una funcióngetItem()
que tome un argumentoInt
y muestre unFlow<Item>
.
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
Se recomienda usar Flow
en la capa de persistencia. Con Flow
como el tipo de datos que se muestra, recibirás una notificación cada vez que cambien los datos de la base de datos. Room
mantiene este Flow
actualizado por ti, lo que significa que solo necesitas obtener los datos de forma explícita una vez. Esta configuración es útil para actualizar la lista de inventario, que implementarás en el siguiente codelab. Debido al tipo de datos que se muestra para Flow
, Room también ejecuta la búsqueda en el subproceso en segundo plano. No necesitas convertirla de manera explícita en una función suspend
ni llamar dentro del alcance de la corrutina.
- Agrega una
@Query
con una funcióngetAllItems()
. - Haz que la consulta de SQLite muestre todas las columnas de la tabla
item
, ordenadas de forma ascendente. - Haz que
getAllItems()
muestre una lista de entidadesItem
comoFlow
.Room
mantiene esteFlow
actualizado por ti, lo que significa que solo necesitas obtener los datos de forma explícita una vez.
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
ItemDao
completado:
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 kotlinx.coroutines.flow.Flow
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
}
- Si bien no notarás ningún cambio visible, compila la app para asegurarte de que no tenga errores.
7. Crea una instancia de base de datos
En esta tarea, crearás una RoomDatabase
que use tu Entity
y tu DAO a partir de las tareas anteriores. La clase de base de datos define la lista de entidades y DAO.
La clase Database
proporciona a tu app instancias de los DAO que definas. A su vez, la app puede usar los DAO para recuperar datos de la base de datos como instancias de objetos de entidad de datos asociados. La app también puede usar las entidades de datos definidas para actualizar filas de las tablas correspondientes o crear filas nuevas para su inserción.
Debes crear una clase abstracta RoomDatabase
y anotarla con @Database
. Esta clase tiene un método que muestra la instancia existente de RoomDatabase
si la base de datos no existe.
Este es el proceso general para obtener la instancia RoomDatabase
:
- Crea una clase
public abstract
que extiendaRoomDatabase
. La nueva clase abstracta que defines actúa como un contenedor de la base de datos. La clase que defines es abstracta porqueRoom
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
ItemDao
, yRoom
genera la implementación por ti. - Solo necesitas una instancia de
RoomDatabase
para toda la app, así que haz queRoomDatabase
sea un singleton. - Usa el
Room.databaseBuilder
deRoom
para crear tu base de datos (item_database
), solo si no existe. De lo contrario, muestra la base de datos existente.
Crea la base de datos
- En el paquete
data
, crea una clase de KotlinInventoryDatabase.kt
. - En el archivo
InventoryDatabase.kt
, haz que la claseInventoryDatabase
sea una claseabstract
que extiendaRoomDatabase
. - Anota la clase con
@Database
. Ignora el error de parámetros faltantes, ya que lo corregirás en el siguiente paso.
import androidx.room.Database
import androidx.room.RoomDatabase
@Database
abstract class InventoryDatabase : RoomDatabase() {}
La anotación @Database
requiere varios argumentos para que Room
pueda compilar la base de datos.
- Especifica el
Item
como la única clase con la lista deentities
. - Establece
version
como1
. Cada vez que cambies el esquema de la tabla de la base de datos, debes aumentar el número de versión. - Establece
exportSchema
comofalse
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 privadaInstance
para la base de datos y, luego, inicialízala ennull
.
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 objetocompanion
, define un métodogetDatabase()
con un parámetroContext
que necesite el compilador de bases de datos. - Muestra un tipo
InventoryDatabase
. Aparecerá un mensaje de error porquegetDatabase()
aún no muestra nada.
import android.content.Context
fun getDatabase(context: Context): InventoryDatabase {}
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. Usa el bloque synchronized{}
para evitar la condición de carrera.
- Dentro de
getDatabase()
, muestra la variableInstance
o, siInstance
es nula, inicialízala dentro de un bloquesynchronized{}
. 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 sincronizado, usa el compilador de bases de datos para obtener la base de datos. Ignora los errores, ya que los corregirás en los próximos pasos.
import androidx.room.Room
Room.databaseBuilder()
- Dentro del bloque
synchronized
, usa el compilador de bases de datos para obtener una base de datos. Pasa aRoom.databaseBuilder()
el contexto de la aplicación, la clase de la base de datos y un nombre para la base de datos,item_database
.
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
Android Studio genera un error de discrepancia de tipos. Para quitar este error, debes agregar una build()
en los pasos siguientes.
- Agrega la estrategia de migración necesaria al compilador. Usa
.
fallbackToDestructiveMigration()
.
.fallbackToDestructiveMigration()
- Para crear la instancia de base de datos, llama a
.build()
. Esta llamada quita los errores de Android Studio.
.build()
- Después de
build()
, agrega un bloquealso
y asignaInstance = it
para mantener una referencia a la instancia de base de datos recién creada.
.also { Instance = it }
- Al final del bloque
synchronized
, muestrainstance
. El código final se ve como el siguiente:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var Instance: InventoryDatabase? = null
fun getDatabase(context: Context): InventoryDatabase {
// if the Instance is not null, return it, otherwise create a new database instance.
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
.build()
.also { Instance = it }
}
}
}
}
- Compila tu código para asegurarte de que no haya errores.
8. Implementa el repositorio
En esta tarea, implementarás la interfaz ItemsRepository
y la clase OfflineItemsRepository
para proporcionar entidades get
, insert
, delete
y update
de la base de datos.
- Abre el archivo
ItemsRepository.kt
bajo el paquetedata
. - Agrega las siguientes funciones a la interfaz, que se asignan a la implementación de DAO.
import kotlinx.coroutines.flow.Flow
/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
/**
* Retrieve all the items from the the given data source.
*/
fun getAllItemsStream(): Flow<List<Item>>
/**
* Retrieve an item from the given data source that matches with the [id].
*/
fun getItemStream(id: Int): Flow<Item?>
/**
* Insert item in the data source
*/
suspend fun insertItem(item: Item)
/**
* Delete item from the data source
*/
suspend fun deleteItem(item: Item)
/**
* Update item in the data source
*/
suspend fun updateItem(item: Item)
}
- Abre el archivo
OfflineItemsRepository.kt
bajo el paquetedata
. - Pasa un parámetro de constructor del tipo
ItemDao
.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
- En la clase
OfflineItemsRepository
, anula las funciones definidas en la interfazItemsRepository
y llama a las funciones correspondientes desde elItemDao
.
import kotlinx.coroutines.flow.Flow
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)
override suspend fun insertItem(item: Item) = itemDao.insert(item)
override suspend fun deleteItem(item: Item) = itemDao.delete(item)
override suspend fun updateItem(item: Item) = itemDao.update(item)
}
Implementa la clase AppContainer
En esta tarea, crearás una instancia de la base de datos y pasarás la instancia de DAO a la clase OfflineItemsRepository
.
- Abre el archivo
AppContainer.kt
bajo el paquetedata
. - Pasa la instancia
ItemDao()
al constructorOfflineItemsRepository
. - Para crear una instancia de la instancia de base de datos, llama a
getDatabase()
en la claseInventoryDatabase
que pasa el contexto y llama a.itemDao()
para crear la instancia deDao
.
override val itemsRepository: ItemsRepository by lazy {
OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}
Ahora tienes todos los componentes básicos para trabajar con tu Room. Este código se compila y se ejecuta, pero no hay forma de saber si realmente funciona. Por lo tanto, este es un buen momento para probar tu base de datos. Para completar la prueba, necesitas que ViewModel
se comunique con la base de datos.
9. Agrega la función de guardar
Hasta ahora, creaste una base de datos, y las clases de IU formaban parte del código de partida. Para guardar los datos transitorios de la app y acceder a la base de datos, debes actualizar los ViewModel
s. Tus elementos ViewModel
interactúan con la base de datos a través del DAO y proporcionan datos a la IU. Todas las operaciones de la base de datos deben ejecutarse fuera del subproceso de IU principal. Para ello, usa corrutinas y viewModelScope
.
Explicación de la clase de estado de la IU
Abre el archivo ui/item/ItemEntryViewModel.kt
. La clase de datos ItemUiState
representa el estado de la IU de un elemento. La clase de datos ItemDetails
representa un solo elemento.
El código de partida te proporciona tres funciones de extensión:
- La función de extensión
ItemDetails.toItem()
convierte el objeto de estado de la IU deItemUiState
en el tipo de entidadItem
. - La función de extensión
Item.toItemUiState()
convierte el objeto de entidadItem
de Room en el tipo de estado de la IU deItemUiState
. - La función de extensión
Item.toItemDetails()
convierte el objeto de entidad de RoomItem
enItemDetails
.
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
val itemDetails: ItemDetails = ItemDetails(),
val isEntryValid: Boolean = false
)
data class ItemDetails(
val id: Int = 0,
val name: String = "",
val price: String = "",
val quantity: String = "",
)
/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
id = id,
name = name,
price = price.toDoubleOrNull() ?: 0.0,
quantity = quantity.toIntOrNull() ?: 0
)
fun Item.formatedPrice(): String {
return NumberFormat.getCurrencyInstance().format(price)
}
/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
itemDetails = this.toItemDetails(),
isEntryValid = isEntryValid
)
/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
id = id,
name = name,
price = price.toString(),
quantity = quantity.toString()
)
Usa la clase anterior en los ViewModels para leer y actualizar la IU.
Actualiza ViewModel de InputEntry
En esta tarea, debes pasar el repositorio al archivo ItemEntryViewModel.kt
. También guardas los detalles del elemento ingresados en la pantalla Add Item en la base de datos.
- Observa la función privada
validateInput()
en la claseItemEntryViewModel
.
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
return with(uiState) {
name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
}
}
La función anterior verifica si name
, price
y quantity
están vacíos. Usarás esta función para verificar la entrada del usuario antes de agregar o actualizar la entidad en la base de datos.
- Abre la clase
ItemEntryViewModel
y agrega un parámetro de constructor predeterminadoprivate
del tipoItemsRepository
.
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
- Actualiza el
initializer
del ViewModel de entrada de elementos enui/AppViewModelProvider.kt
y pasa la instancia del repositorio como parámetro.
object AppViewModelProvider {
val Factory = viewModelFactory {
// Other Initializers
// Initializer for ItemEntryViewModel
initializer {
ItemEntryViewModel(inventoryApplication().container.itemsRepository)
}
//...
}
}
- Ve al archivo
ItemEntryViewModel.kt
y, al final de la claseItemEntryViewModel
, agrega una función de suspensión llamadasaveItem()
para insertar un elemento en la base de datos de Room. Esta función agrega los datos a la base de datos sin bloqueos.
suspend fun saveItem() {
}
- Dentro de la función, verifica si
itemUiState
es válido y conviértelo al tipoItem
para que Room pueda comprender los datos. - Llama a
insertItem()
enitemsRepository
y pasa los datos. La IU llama a esta función para agregar detalles del elemento a la base de datos.
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
Agregaste todas las funciones necesarias para incluir entidades en la base de datos. En la siguiente tarea, actualizarás la IU para usar las funciones anteriores.
Explicación del elemento componible ItemEntryBody()
- En el archivo
ui/item/ItemEntryScreen.kt
, el elementoItemEntryBody()
componible se implementa de forma parcial como parte del código de partida. Observa el elementoItemEntryBody()
componible en la llamada a funciónItemEntryScreen()
.
// No need to copy over, part of the starter code
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
.fillMaxWidth()
)
- Ten en cuenta que el estado de la IU y la lambda
updateUiState
se pasan como parámetros de función. Mira la definición de la función para ver cómo se actualiza el estado de la IU.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
itemUiState: ItemUiState,
onItemValueChange: (ItemUiState) -> Unit,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
onValueChange = onItemValueChange,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = onSaveClick,
enabled = itemUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.save_action))
}
}
}
Estás mostrando ItemInputForm
y un botón Save en este elemento componible. En el elemento componible ItemInputForm()
, se muestran tres campos de texto. El botón Save solo está habilitado si se ingresa texto en los campos de texto. El valor isEntryValid
es "true" si el texto de todos los campos es válido (no está vacío).
- Observa la implementación de la función de componibilidad
ItemInputForm()
y el parámetro de la funciónonValueChange
. Estás actualizando el valor deitemDetails
con el valor que ingresó el usuario en los campos de texto. Cuando el botón Save está habilitado,itemUiState.itemDetails
tiene los valores que se deben guardar.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
//...
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
//...
)
//...
}
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
itemDetails: ItemDetails,
modifier: Modifier = Modifier,
onValueChange: (ItemUiState) -> Unit = {},
enabled: Boolean = true
) {
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = itemUiState.name,
onValueChange = { onValueChange(itemDetails.copy(name = it)) },
//...
)
OutlinedTextField(
value = itemUiState.price,
onValueChange = { onValueChange(itemDetails.copy(price = it)) },
//...
)
OutlinedTextField(
value = itemUiState.quantity,
onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
//...
)
}
}
Agrega un objeto de escucha de clics al botón Save
Para vincular todo, agrega un controlador de clics al botón Save. Dentro del controlador de clics, inicias una corrutina y llamas a saveItem()
para guardar los datos en la base de datos de Room.
- En
ItemEntryScreen.kt
, dentro de la función de componibilidadItemEntryScreen
, crea unval
llamadocoroutineScope
con la función de componibilidadrememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Actualiza la llamada a la función
ItemEntryBody
()
y, luego, inicia una corrutina dentro de la lambdaonSaveClick
.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
- Observa la implementación de la función
saveItem()
en el archivoItemEntryViewModel.kt
para verificar siitemUiState
es válido a través de la conversión deitemUiState
en el tipoItem
y, luego, su inserción en la base de datos conitemsRepository.insertItem()
.
// No need to copy over, you have already implemented this as part of the Room implementation
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
- En
ItemEntryScreen.kt
, dentro de la función de componibilidadItemEntryScreen
, dentro de la corrutina, llama aviewModel.saveItem()
para guardar el elemento en la base de datos.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
}
},
//...
)
Observa que no usaste viewModelScope.launch()
para saveItem()
en el archivo ItemEntryViewModel.kt
, pero es necesario para ItemEntryBody
()
cuando llamas a un método de repositorio. Solo puedes llamar a funciones de suspensión desde una corrutina o desde otra función de suspensión. La función viewModel.saveItem()
es de suspensión.
- Compila y ejecuta tu app.
- Presiona el BAF +.
- En la pantalla Add Item, agrega los detalles del elemento y presiona Save. Observa que, si presionas el botón Save, no se cierra la pantalla Add Item.
- En la lambda
onSaveClick
, agrega una llamada anavigateBack()
después de la llamada aviewModel.saveItem()
para volver a la pantalla anterior. Tu funciónItemEntryBody()
se ve como el siguiente código:
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- Vuelve a ejecutar la app y realiza los mismos pasos para ingresar y guardar los datos. Observa que, esta vez, la app regresa a la pantalla Inventory.
Esta acción guarda los datos, pero no puede ver los datos de inventario en la app. En la próxima tarea, usarás el Inspector de bases de datos para ver los datos que guardaste.
10. Consulta el contenido de la base de datos con el Inspector de bases de datos
El Inspector de bases de datos te permite inspeccionar, consultar y modificar las bases de datos de tu app mientras se ejecuta. Esto es particularmente útil para depurar bases de datos. El Inspector de bases de datos funciona con SQLite simple y bibliotecas compiladas sobre SQLite, como Room. El Inspector de bases de datos funciona mejor en emuladores o dispositivos que ejecutan el nivel de API 26.
- Si aún no lo hiciste, ejecuta tu app en un emulador o un dispositivo conectado con nivel de API 26 o superior.
- En Android Studio, selecciona View > Tool Windows > App Inspection en la barra de menú.
- Elige la pestaña Database Inspector.
- En el panel Database Inspector, selecciona
com.example.inventory
en el menú desplegable si aún no está seleccionado. item_database de la app de Inventory aparece en el panel Databases.
- Expande el nodo de item_database del panel Databases y selecciona Item para inspeccionarlo. Si el panel Databases está vacío, usa el emulador para agregar algunos elementos a la base de datos con la pantalla Add Item.
- Marca la casilla de verificación Live updates en el Inspector de bases de datos para actualizar automáticamente los datos que presenta mientras interactúas con tu app en ejecución en el emulador o dispositivo.
¡Felicitaciones! Creaste una app que puede conservar datos con Room. En el siguiente codelab, agregarás una lazyColumn
a tu app para mostrar los elementos en la base de datos y agregar nuevas funciones a la app, como la capacidad de borrar y actualizar las entidades. ¡Nos vemos!
11. Obtén el código de la solución
El código de solución para este codelab se encuentra en el repositorio de GitHub. Para descargar el código del codelab terminado, usa los siguientes comandos de Git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout room
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de la solución para este codelab, míralo en GitHub.
12. Resumen
- Define tus tablas como clases de datos anotadas con
@Entity
. Define las propiedades anotadas con@ColumnInfo
como columnas en las tablas. - Define un objeto de acceso a datos (DAO) como una interfaz anotada con
@Dao
. El DAO asigna funciones de Kotlin a consultas de bases de datos. - Usa anotaciones para definir las funciones
@Insert
,@Delete
y@Update
. - Usa la anotación
@Query
con una cadena de consulta de SQLite como parámetro para cualquier otra consulta. - Usa el Inspector de bases de datos para ver los datos guardados en la base de datos SQLite de Android.
13. Más información
Documentación para desarrolladores de Android
- Cómo guardar contenido en una base de datos local con Room
- androidx.room
- Cómo depurar tu base de datos con el Inspector de bases de datos
Entradas de blog
Videos
Otros artículos y documentación