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,IconButtonyLazyColumn - Saber usar el elemento componible
NavHostpara definir rutas y pantallas en tu app - Saber navegar entre pantallas con un
NavHostController - Conocer el componente de la arquitectura de Android
ViewModely saber usarViewModelProvider.Factorypara 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
- El código de partida 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 partida
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 usando 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 DAOs 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
dataen 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
valovar. - Las clases de datos no pueden ser
abstract,opennisealed.
Para obtener más información sobre las clases de datos, consulta la documentación correspondiente.
- Prefija la definición de la clase
Itemcon la palabra clavedatapara 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 argumentotableNamepara estableceritemscomo el nombre de la tabla de SQLite.
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
- Anota la propiedad
idcon@PrimaryKeypara queidsea 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
idun valor predeterminado de0, que es necesario para queidgenere automáticamente valores deid. - Agrega el parámetro
autoGeneratea la anotación@PrimaryKeypara especificar si la columna de clave primaria se debe generar de forma automática. SiautoGenerateestá 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,
// ...
)
¡Excelente! 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
ItemDaocon@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 delitemde la claseEntitycomo su argumento. - Marca la función con la palabra clave
suspendpara 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
onConflicty 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
@Updateque tome unItemcomo 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
idespecificado. El siguiente código proporciona una consulta de muestra que selecciona todas las columnas deitems, dondeidcoincide con un valor específico yides 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
Stringa@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 argumentoInty 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
@Querycon 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 entidadesItemcomoFlow.Roommantiene esteFlowactualizado 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 abstractque extiendaRoomDatabase. La nueva clase abstracta que defines actúa como un contenedor de la base de datos. La clase que defines es abstracta porqueRoomcrea 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, yRoomgenera la implementación por ti. - Solo necesitas una instancia de
RoomDatabasepara toda la app, así que haz queRoomDatabasesea un singleton. - Usa el
Room.databaseBuilderdeRoompara 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 claseInventoryDatabasesea una claseabstractque 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
Itemcomo la única clase con la lista deentities. - Establece
versioncomo1. Cada vez que cambies el esquema de la tabla de la base de datos, debes aumentar el número de versión. - Establece
exportSchemacomofalsepara 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
ItemDaode 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 privadaInstancepara 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
Instancecon@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ámetroContextque 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 variableInstanceo, siInstancees 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 bloquealsoy asignaInstance = itpara 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.ktbajo 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.ktbajo 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 interfazItemsRepositoryy 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.ktbajo el paquetedata. - Pasa la instancia
ItemDao()al constructorOfflineItemsRepository. - Para crear una instancia de la instancia de base de datos, llama a
getDatabase()en la claseInventoryDatabaseque 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 ViewModels. 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 deItemUiStateen el tipo de entidadItem. - La función de extensión
Item.toItemUiState()convierte el objeto de entidadItemde Room en el tipo de estado de la IU deItemUiState. - La función de extensión
Item.toItemDetails()convierte el objeto de entidad de RoomItemenItemDetails.
// 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
ItemEntryViewModely agrega un parámetro de constructor predeterminadoprivatedel tipoItemsRepository.
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
- Actualiza el
initializerdel ViewModel de entrada de elementos enui/AppViewModelProvider.kty 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.kty, 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
itemUiStatees válido y conviértelo al tipoItempara que Room pueda comprender los datos. - Llama a
insertItem()enitemsRepositoryy 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, la función de componibilidadItemEntryBody()se implementa de forma parcial como parte del código de partida. Observa la función de componibilidadItemEntryBody()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
updateUiStatese 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 esta función de componibilidad. 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 deitemDetailscon el valor que ingresó el usuario en los campos de texto. Cuando el botón Save está habilitado,itemUiState.itemDetailstiene 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 unvalllamadocoroutineScopecon 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.ktpara verificar siitemUiStatees válido a través de la conversión deitemUiStateen el tipoItemy, 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.inventoryen 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@ColumnInfocomo 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,@Deletey@Update. - Usa la anotación
@Querycon 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



