1. Antes de comenzar
La mayoría de las apps de calidad de producción tienen datos que deben guardarse incluso después de que el usuario cierra la app. 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 implementar, configurar y usar la base de datos. Room también proporciona comprobaciones de tiempo de compilación de las sentencias de SQLite.
En la siguiente imagen, se puede apreciar cómo Room se ajusta a la arquitectura general recomendada en este curso.
Requisitos previos
- Debes saber compilar una interfaz de usuario (IU) básica de una app para Android.
- Debes saber usar actividades, fragmentos y vistas.
- Debes saber navegar entre fragmentos con Safe Args para pasar datos entre fragmentos.
- Debes estar familiarizado con los componentes de arquitectura de Android
ViewModel
,LiveData
yFlow
, y saber cómo usarViewModelProvider.Factory
para crear una instancia de ViewModels. - Debes estar familiarizado con los conceptos básicos de simultaneidad.
- Debes saber cómo usar corrutinas para tareas de larga duración.
- Debes tener conocimientos básicos de bases de datos SQL y el lenguaje SQLite.
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 DAO y clases de bases de datos
- Cómo usar un objeto de acceso a datos (DAO) para asignar funciones de Kotlin a consultas de SQL
Qué compilarás
- Compilarás una app de inventario que guarde elementos en la base de datos SQLite.
Requisitos
- Código de inicio de la app de Inventory
- Una computadora que tenga Android Studio instalado
2. Descripción general de la app
En este codelab, trabajarás con una app de inicio llamada Inventory y agregarás la capa de la base de datos en ella por medio de la biblioteca Room. La versión final de la app muestra una lista de elementos de la base de datos de inventario mediante una RecyclerView
. Tendrás opciones para agregar un elemento nuevo, actualizar uno existente y borrar uno de la base de datos de inventario (completarás la funcionalidad de la app en el siguiente codelab).
A continuación, se muestran capturas de pantalla de la versión final de la app.
3. Descripción general de la app de inicio
Descarga el código de inicio para este codelab
Este codelab te brinda el código de inicio para que extiendas con funciones que se explicaron en este codelab. El código de inicio puede contener código que te resulte familiar de anteriores codelabs, y también código que no te resulte familiar y que aprenderás en posteriores codelabs.
Si usas el código de inicio de GitHub, ten en cuenta que el nombre de la carpeta es android-basics-kotlin-inventory-app-starter
. Selecciona esta carpeta cuando abras el proyecto en Android Studio.
A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:
Obtén el código
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
Descripción general del código de inicio
- Abre el proyecto con el código de inicio en Android Studio.
- Ejecuta la app en un dispositivo Android o en un emulador. Asegúrate de que el emulador o dispositivo conectado ejecute el nivel de API 26 o uno superior. El Inspector de bases de datos funciona mejor en el emulador o dispositivos que ejecutan el nivel de API 26.
- La app no muestra datos de inventario. Observa el BAF para agregar elementos nuevos a la base de datos.
- Haz clic en él. La app navega a una pantalla nueva en la que puedes ingresar los detalles del elemento nuevo.
Problemas con el código de inicio
- En la pantalla Add Item, ingresa los detalles de un elemento. Presiona Save. El fragmento de artículo agregado no se cerrará. Deberás navegar hacia atrás con la tecla correspondiente del sistema. El nuevo elemento no se guardará ni aparecerá en la pantalla del inventario. Ten en cuenta que la app está incompleta y no se implementa la funcionalidad del botón Save.
En este codelab, agregarás la parte de la base de datos de una app que guarda los detalles del inventario en la base de datos SQLite. Utilizarás la biblioteca de persistencias Room para interactuar con la base de datos SQLite.
Explicación del código
El código de inicio que descargaste tiene diseños de pantalla que ya están confeccionados. En esta ruta de aprendizaje, te enfocarás en implementar la lógica de la base de datos. Aquí encontrarás una breve explicación de algunos de los archivos para comenzar.
main_activity.xml
Es la actividad principal que aloja todos los demás fragmentos de la app. El método onCreate()
recupera NavController
de NavHostFragment
y configura la barra de acciones para usarla con NavController
.
item_list_fragment.xml
Es la primera pantalla que se muestra en la app. Contiene principalmente una RecyclerView y un BAF. Implementarás RecyclerView más adelante en la ruta de aprendizaje.
fragment_add_item.xml
Este diseño contiene campos de texto para ingresar los detalles del elemento de inventario nuevo que se agregará.
ItemListFragment.kt
Este fragmento contiene código estándar en su mayoría. En el método onViewCreated()
, el objeto de escucha de clics está configurado como BAF para navegar al fragmento de Add Item.
AddItemFragment.kt
Este fragmento se usa para agregar nuevos elementos a la base de datos. La función onCreateView()
inicializa la variable de vinculación y la función onDestroyView()
oculta el teclado antes de destruir el fragmento.
4. Componentes principales de Room
Kotlin ofrece una manera fácil de tratar con datos mediante la introducción de clases de datos. Se puede acceder a esos datos y modificarlos con llamadas a función. Sin embargo, en el mundo de las bases de datos, necesitas tablas y consultas para acceder a datos y modificarlos. Los siguientes componentes de Room facilitan estos flujos de trabajo.
Estos son los tres componentes principales de Room:
- Las entidades de datos representan tablas de la base de datos de tu app. Se usan a fin de actualizar los datos almacenados en filas dentro de tablas y crear filas nuevas para insertarlas.
- Los objetos de acceso a datos (DAO) proporcionan métodos que tu app usa para recuperar, actualizar, insertar y borrar datos en la base de datos.
- La clase de base de datos contiene la base de datos y es el punto de acceso principal para la conexión subyacente a la base de datos de la app. La clase de base de datos proporciona a tu app instancias de los DAO asociados con esa base de datos.
Implementarás y obtendrás más información sobre estos componentes más adelante en el codelab. En el siguiente diagrama, se muestra cómo los componentes de Room funcionan en conjunto para interactuar con la base de datos.
Agrega bibliotecas Room
En esta tarea, agregarás las bibliotecas de componentes Room requeridas a tus archivos de Gradle.
- Abre el archivo de nivel de módulo de Gradle,
build.gradle (Module: InventoryApp.app)
. En el bloquedependencies
, agrega las siguientes dependencias para la biblioteca Room.
// Room implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version"
5. Crea un elemento Entity
La 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 conservará la información sobre los elementos del inventario, como el nombre, el precio y las acciones disponibles.
La anotación @Entity
marca una clase como una clase Entity de base de datos. Por cada clase Entity, se crea una tabla de base de datos para contener 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 asignada, la 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. Define los campos para almacenar la siguiente información de inventario de cada artículo.
- Una
Int
para almacenar la clave primaria - Una
String
para almacenar el nombre del elemento - Un
double
para almacenar el precio del artículo - Una
Int
para almacenar la cantidad en stock
- Abre el código de inicio en Android Studio.
- Crea un paquete llamado
data
en el paquete basecom.example.inventory
.
- En el paquete
data
, crea una nueva clase de Kotlin llamadaItem
. Esta clase representará una entidad de base de datos en tu app. En el siguiente paso, agregarás los campos correspondientes para almacenar la información del inventario. - Actualiza la definición de clase
Item
con el siguiente código. Declaraid
de tipoInt
,itemName
de tipoString,
,itemPrice
de tipoDouble
yquantityInStock
de tipoInt
como parámetros para el constructor principal. Asigna aid
un valor predeterminado de0
. Esta será la clave primaria, un ID para identificar de manera única cada registro o entrada en la tablaItem
.
class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
Clases de datos
Las clases de datos se usan principalmente para conservar datos en Kotlin. Se marcan con la palabra clave data
. Los objetos de clase de datos de Kotlin tienen beneficios adicionales: el compilador genera automáticamente utilidades para comparar, mostrar y copiar contenido como toString()
, copy()
y equals()
.
Ejemplo:
// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}
Para garantizar la coherencia y el comportamiento significativo del código generado, las clases de datos deben cumplir los siguientes requisitos:
- El constructor principal debe tener al menos un parámetro.
- Todos los parámetros del constructor principal deben marcarse como
val
ovar
. - Las clases de datos no pueden ser
abstract
,open
,sealed
niinner
.
Para obtener más información sobre las clases de datos, consulta la documentación.
- Para convertir la clase
Item
en una clase de datos, puedes agregar al prefijo su definición de clase con la palabra clavedata
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
- Sobre la declaración de clase
Item
, anota la clase de datos con@Entity
. Usa el argumentotableName
para asignar elitem
como el nombre de la tabla de SQLite.
@Entity(tableName = "item")
data class Item(
...
)
- Para identificar
id
como la clave primaria, anota la propiedadid
con@PrimaryKey
. Establece el parámetroautoGenerate
entrue
para queRoom
genere el ID de cada entidad. Esto garantiza que el ID de cada artículo sea único.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
...
)
- Anota las propiedades restantes con
@ColumnInfo
. La anotaciónColumnInfo
se usa para personalizar la columna asociada con el campo en particular. Por ejemplo, cuando usas el argumentoname
, puedes especificar un nombre de columna diferente para el campo, en lugar del nombre de variable. Personaliza los nombres de las propiedades con los parámetros que se muestran a continuación. Este enfoque es similar a usartableName
para especificar un nombre diferente en la base de datos.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
6. Crea el elemento DAO
Objeto de acceso a datos (DAO)
El objeto de acceso a datos (DAO) es un patrón que se usa para separar la capa de persistencia con el 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 subyacente del resto de la aplicación. Esto permite cambiar la capa de acceso a los datos independientemente del código que usa los datos.
En esta tarea, definirás un objeto de acceso a datos (DAO) para Room. Los objetos de acceso a datos son los componentes principales de Room que son responsables de definir la interfaz que accede a la base de datos.
El DAO que crearás es una interfaz personalizada que proporciona métodos convenientes para consultar/recuperar, insertar, borrar y actualizar la base de datos. Room generará una implementación de esta clase en el tiempo de compilación.
Para las operaciones de base de datos comunes, la biblioteca Room
proporciona anotaciones de conveniencia, como @Insert
, @Delete
y @Update
. Para todo lo demás, se encuentra la anotación @Query
. Puedes escribir cualquier consulta que sea compatible con SQLite.
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 inventario, 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
Ahora, implementa el elemento DAO en tu app:
- En el paquete
data
, crea la clase de KotlinItemDao.kt
. - Cambia la definición de clase a
interface
y anótala con@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 de la claseEntity
item
como su argumento. Las operaciones de la base de datos pueden demorar mucho tiempo en ejecutarse, por lo que deben hacerlo en un subproceso independiente. Convierte la función en una de suspensión para que se la pueda llamar desde una corrutina.
@Insert
suspend fun insert(item: Item)
- Agrega un argumento
OnConflict
y asígnale un valor deOnConflictStrategy.
IGNORE
. El argumentoOnConflict
le indica a Room qué hacer en caso de conflicto. La estrategiaOnConflictStrategy.
IGNORE
omite un elemento nuevo si ya es clave primaria en la base de datos. Para obtener más información sobre las estrategias de conflicto disponibles, consulta la documentación.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
Ahora Room
generará todo el código necesario para insertar item
en la base de datos. Cuando llamas a insert()
desde tu código Kotlin, Room
ejecuta una consulta de SQL para insertar la entidad en la base de datos. (Nota: La función puede tener el nombre que desees; no es necesario que se llame insert()
).
- Agrega una anotación
@Update
con una funciónupdate()
para unitem
. La entidad que se actualiza tiene la misma clave que la que se pasa. Puedes actualizar algunas o todas las demás propiedades de la entidad. Al igual que con el métodoinsert()
, crea el siguiente métodoupdate()
suspend
.
@Update
suspend fun update(item: Item)
- Agrega la anotación
@Delete
con una funcióndelete()
para borrar elementos. Haz que sea un método de suspensión. La anotación@Delete
borra un elemento o una lista de elementos. (Nota: Debes pasar las entidades que se van a borrar. Si no las tienes, es posible que tengas que recuperarlas antes de llamar a la funcióndelete()
).
@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. Luego, agregarás anotaciones de Room y usarás una versión modificada de la siguiente consulta en los pasos posteriores. En los próximos pasos, también lo cambiarás a un método DAO por medio de Room. - Selecciona todas las columnas de
item
. WHERE
(id
) coincide con un valor específico.
Ejemplo:
SELECT * from item WHERE id = 1
- Cambia la consulta de SQL anterior para usarla con la anotación de Room y un argumento. Agrega una anotación
@Query
y proporciona la consulta como un parámetro de string a la anotación@Query
. Agrega un parámetroString
a@Query
, que es una consulta de SQLite para recuperar un elemento de la tabla correspondiente. - Selecciona todas las columnas de
item
. WHERE
(id
) coincide con el argumento :id
. Observa el objeto:id
. Usa la notación de dos puntos en la consulta para hacer referencia a argumentos en la función.
@Query("SELECT * from item WHERE id = :id")
- Debajo de la anotación
@Query
, agregagetItem()
, que toma un argumentoInt
y muestra unaFlow<Item>
.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
Usar Flow
o LiveData
como tipo de datos garantizará que se te notifique cuando cambien los datos de la base de datos. Se recomienda usar Flow
en la capa de persistencia. Room
mantiene este Flow
actualizado por ti, lo que significa que solo necesitas obtener los datos de forma explícita una vez. Esto 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.
Es posible que debas importar Flow
de kotlinx.coroutines.flow.Flow
.
- Agrega una
@Query
con una funcióngetItems()
: - Haz que la consulta de SQLite muestre todas las columnas de la tabla
item
, ordenadas de forma ascendente. - Haz que
getItems()
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 item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- Si bien no notarás ningún cambio visible, ejecuta 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 usa Entity
y el DAO que creaste en la tarea anterior. La clase de la base de datos define la lista de entidades y objetos de acceso a datos. También es el punto de acceso principal para la conexión subyacente.
La clase Database
proporciona a tu app las instancias de los DAO que definiste. 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 a fin de actualizar filas de las tablas correspondientes o crear filas nuevas para su inserción.
Debes crear una clase abstracta RoomDatabase
anotada con @Database
. Esta clase tiene un método que crea una instancia de RoomDatabase
si no existe, o bien muestra la instancia existente de RoomDatabase
.
Este es el proceso general para obtener la instancia RoomDatabase
:
- Crea una clase
public abstract
que extiendaRoomDatabase
. La nueva clase abstracta que definiste actúa como un contenedor de la base de datos. La clase que definiste 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
generará 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 KotlinItemRoomDatabase.kt
. - En el archivo
ItemRoomDatabase.kt
, crea la claseItemRoomDatabase
como una claseabstract
que extiendaRoomDatabase
. Anota la clase con@Database
. En el siguiente paso, corregirás el error de parámetros faltantes.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
- La anotación
@Database
requiere varios argumentos para queRoom
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, deberás 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)
- La base de datos necesita saber sobre el DAO. Dentro del cuerpo de la clase, declara una función abstracta que muestre el
ItemDao
. Puedes tener varios DAO.
abstract fun itemDao(): ItemDao
- Debajo de la función abstracta, define un objeto
companion
. El objeto complementario permite el acceso a los métodos para crear u obtener la base de datos con el nombre de clase como calificador.
companion object {}
- Dentro del objeto
companion
, declara una variable anulable privadaINSTANCE
para la base de datos y, luego, inicializala ennull
. La variableINSTANCE
mantendrá una referencia a la base de datos, cuando se cree 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 almacenará en caché, y todas las operaciones de escritura y lectura se realizarán desde y hacia la memoria principal. Esto ayuda 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: ItemRoomDatabase? = 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 tipoItemRoomDatabase
. Verás un error porquegetDatabase()
todavía no muestra nada.
fun getDatabase(context: Context): ItemRoomDatabase {}
- Es posible que varios subprocesos se ejecuten en una condición de carrera y soliciten una instancia de base de datos al mismo tiempo, lo que genera dos bases de datos en lugar de una. 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 el objeto complementario this
que deseas bloquear dentro del bloque de función. Solucionarás el error en los pasos posteriores.
return INSTANCE ?: synchronized(this) { }
- Dentro del bloque sincronizado, crea una variable de instancia
val
y usa el generador de bases de datos para obtener la base de datos. Seguirás teniendo errores que solucionarás en los próximos pasos.
val instance = Room.databaseBuilder()
- Al final del bloque
synchronized
, muestrainstance
.
return instance
- Dentro del bloque
synchronized
, inicializa la variableinstance
y 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
.
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
Android Studio generará un error de tipo de coincidencia. Para quitarlo, deberás agregar una estrategia de migración y build()
en los pasos siguientes.
- Agrega la estrategia de migración necesaria al compilador. Utiliza
.fallbackToDestructiveMigration()
.
Normalmente, tendrías que proporcionar un objeto de migración con una estrategia para cuando cambie el esquema. Un objeto de migración es un objeto que define cómo tomas todas las filas con el esquema anterior y las conviertes en filas en el esquema nuevo para que no se pierdan datos. La migración no se incluye en este codelab. Una solución simple es destruir y volver a compilar la base de datos, lo que significa que los datos se pierden.
.fallbackToDestructiveMigration()
- Para crear la instancia de base de datos, llama a
.build()
. Esto debería quitar los errores de Android Studio.
.build()
- Dentro del bloque
synchronized
, asignaINSTANCE = instance
.
INSTANCE = instance
- Al final del bloque
synchronized
, muestrainstance
. El código final debería verse así:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
- Compila tu código para asegurarte de que no haya errores.
Implementa la clase Application
En esta tarea, crearás una instancia de la instancia de base de datos en la clase Application.
- Abre
InventoryApplication.kt
y crea unval
llamadodatabase
del tipoItemRoomDatabase
. Para crear una instancia de la instanciadatabase
, llama agetDatabase()
enItemRoomDatabase
y pasa el contexto. Usa el delegadolazy
para que la instanciadatabase
se cree de forma diferida cuando necesites o consultes la referencia por primera vez (en lugar de hacerlo cuando se inicie la app). Esta acción creará la base de datos (la base de datos física en el disco) en el primer acceso.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
class InventoryApplication : Application(){
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
Usarás esta instancia de database
más adelante en el codelab cuando crees una instancia de ViewModel.
Ahora tienes todos los componentes básicos para trabajar con Room. Este código se compila y se ejecuta, pero no puedes distinguir si funciona realmente. Este es un buen momento para agregar un elemento nuevo a tu base de datos de Inventory a fin de probarla. Para ello, necesitas un ViewModel
a fin de comunicarte con la base de datos.
8. Agrega un ViewModel
Hasta ahora, creaste una base de datos, y las clases de IU formaban parte del código de inicio. Para guardar los datos transitorios de la app y acceder a la base de datos, necesitas un ViewModel. Tu ViewModel de inventario interactuará con la base de datos a través del DAO y proporcionará datos a la IU. Todas las operaciones de la base de datos se deberán ejecutar desde el subproceso de IU principal. Para ello, deberás usar corrutinas y viewModelScope
.
Crea el ViewModel de Inventory
- En el paquete
com.example.inventory
, crea un archivo de claseInventoryViewModel.kt
de Kotlin. - Extiende la clase
InventoryViewModel
desdeViewModel
. Pasa el objetoItemDao
como parámetro al constructor predeterminado.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- Al final del archivo
InventoryViewModel.kt
fuera de la clase, agrega la claseInventoryViewModelFactory
para crear una instancia deInventoryViewModel
. Pasa el mismo parámetro de constructor queInventoryViewModel
de la instanciaItemDao
. Extiende la clase de la claseViewModelProvider.Factory
. Corregirás el error relacionado con los métodos no implementados en el siguiente paso.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- Haz clic en la bombilla roja y seleccionaImplement Members, o puedes anular el método
create()
dentro deViewModelProvider.Factory
como se muestra a continuación, que toma cualquier tipo de clase como argumento y muestra un objetoViewModel
.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
- Implementa el método
create()
. Verifica simodelClass
es igual a la claseInventoryViewModel
y muestra una instancia de esta. De lo contrario, arroja una excepción.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
Propaga el ViewModel
En esta tarea, propagarás la clase InventoryViewModel
para agregar datos de inventario a la base de datos. Observa la entidad Item
y la pantalla Add Item en la app de Inventory.
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
Necesitas el nombre, el precio y el stock a mano para ese artículo en particular a fin de agregar una entidad a la base de datos. Más adelante en el codelab, usarás la pantalla Add Item para obtener esos detalles del usuario. En la tarea actual, puedes usar tres strings como entrada para ViewModel, convertirlas en una instancia de entidad Item
y guardarla en la base de datos con la instancia ItemDao
. Es momento de implementar todo eso.
- En la clase
InventoryViewModel
, agrega una funciónprivate
llamadainsertItem()
que reciba un objetoItem
y agregue los datos a la base de datos sin bloqueos.
private fun insertItem(item: Item) {
}
- Para interactuar con la base de datos fuera del subproceso principal, inicia una corrutina y llama al método del DAO dentro de ella. Dentro del método
insertItem()
, usaviewModelScope.launch
para iniciar una corrutina en elViewModelScope
. Dentro de la función de inicio, llama a la función de suspensióninsert()
enitemDao
y pasa elitem
.ViewModelScope
es una propiedad de extensión de la claseViewModel
que cancela automáticamente sus corrutinas secundarias cuando se destruyeViewModel
.
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
Importa kotlinx.coroutines.launch,
androidx.lifecycle.
viewModelScope
com.example.inventory.data.Item
, si no se importa de forma automática.
- En la clase
InventoryViewModel
, agrega otra función privada que tome tres strings y muestre una instancia deItem
.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- Dentro de la clase
InventoryViewModel
, agrega una función pública llamadaaddNewItem()
que contenga tres strings para los detalles del elemento. Pasa strings de detalles del elemento a la funcióngetNewItemEntry()
y asigna el valor que se muestra a un valor llamadonewItem
. Llama ainsertItem()
y pasanewItem
para agregar la entidad nueva a la base de datos. Se llamará a este fragmento desde el fragmento de la IU para agregar detalles del elemento a la base de datos.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
Ten en cuenta que no usaste viewModelScope.launch
para addNewItem()
, pero es necesario en insertItem()
cuando llames a un método de DAO. Esto se debe a que solo se permite llamar a las funciones de corrutina desde una corrutina o desde otra función de suspensión. La función itemDao.insert(item)
es de suspensión.
Agregaste todas las funciones necesarias para incluir entidades en la base de datos. En la siguiente tarea, actualizarás el fragmento Add Item para usar las funciones anteriores.
9. Actualiza AddItemFragment
- En
AddItemFragment.kt
, al comienzo de la claseAddItemFragment
, crea unprivate val
llamadoviewModel
del tipoInventoryViewModel
. Usa el delegado de propiedadesby activityViewModels()
de Kotlin para compartirViewModel
en todos los fragmentos. Solucionarás el error en el siguiente paso.
private val viewModel: InventoryViewModel by activityViewModels {
}
- Dentro de la expresión lambda, llama al constructor
InventoryViewModelFactory()
y pasa la instanciaItemDao
. Usa la instanciadatabase
que creaste en una de las tareas anteriores para llamar al constructoritemDao
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
- Debajo de la definición
viewModel
, crea unlateinit var
llamadoitem
del tipoItem
.
lateinit var item: Item
- La pantalla Add Item contiene tres campos de texto para que el usuario obtenga los detalles del elemento. En este paso, agregarás una función para verificar si el texto en TextFields no está vacío. Usarás esta función para verificar la entrada del usuario antes de agregar o actualizar la entidad en la base de datos. Esta validación se debe realizar en el
ViewModel
y no en el fragmento. En la claseInventoryViewModel
, agrega la siguiente funciónpublic
llamadaisEntryValid()
.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
- En
AddItemFragment.kt
, debajo de la funciónonCreateView()
, crea una funciónprivate
llamadaisEntryValid()
que muestre unBoolean
. Corregirás el error que indica el valor de devolución que falta en el siguiente paso.
private fun isEntryValid(): Boolean {
}
- En la clase
AddItemFragment
, implementa la funciónisEntryValid()
. Llama a la funciónisEntryValid()
en la instanciaviewModel
y pasa el texto de las vistas de texto. Muestra el valor de la funciónviewModel.isEntryValid()
.
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
- En la clase
AddItemFragment
, debajo de la funciónisEntryValid()
, agrega otra funciónprivate
llamadaaddNewItem()
sin parámetros y que no muestra nada. Dentro de la función, llama aisEntryValid()
dentro de la condiciónif
.
private fun addNewItem() {
if (isEntryValid()) {
}
}
- Dentro del bloque
if
, llama al métodoaddNewItem()
en la instanciaviewModel
. En los detalles del elemento que ingresa el usuario, utiliza la instanciabinding
para leerlos.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
- Debajo del bloque
if
, crea unval
action
para volver aItemListFragment
. Llama afindNavController
().navigate()
y pasa laaction
.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
Importa androidx.navigation.fragment.findNavController.
- El método completo debería verse de la siguiente manera.
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- Para vincular todo, agrega un controlador de clics al botón Save. En la clase
AddItemFragment
, sobre la funciónonDestroyView()
, anulaonViewCreated()
. - Dentro de la función
onViewCreated()
, agrega un controlador de clics al botón Save y llama aaddNewItem()
desde él.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- Compila y ejecuta la app. Presiona el BAF +. En la pantalla Add Item, agrega los detalles del elemento y presiona Save. Esta acción guarda los datos, pero no puedes ver nada en la app. En la próxima tarea, usarás el Inspector de bases de datos para ver los datos que guardaste.
Consulta la base de datos con el Inspector de bases de datos
- Si aún no lo hiciste, ejecuta tu app en un emulador o un dispositivo conectado con nivel de API 26 o posterior. El Inspector de bases de datos funciona mejor en el emulador o dispositivos que ejecutan el nivel de API 26.
- En Android Studio, selecciona View > Tool Windows > Database Inspector en la barra de menú.
- En el panel del Inspector de bases de datos, selecciona
com.example.inventory
en el menú desplegable. - item_database de la app de Inventory aparece en el panel Databases. Expande el nodo de item_database y selecciona Item a fin de 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 RecyclerView
a tu app para mostrar los elementos en la base de datos y agregar nuevas funciones a la app, como borrar y actualizar entidades. ¡Nos vemos!
10. Código de solución
El código de la solución para este codelab se encuentra en el repositorio y la rama de GitHub que se muestran a continuación.
A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:
Obtén el código
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
11. 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 string 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.
12. 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