Cómo conservar datos con Room

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.

7521165e051cc0d4.png

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 y Flow, y saber cómo usar ViewModelProvider.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

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.

52087556378ea8db.png

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

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

5b0a76c50478a73f.png

  1. 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.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. 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

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

21f3eec988dcfbe9.png

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 11c34fc5e516fb1c.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.

Descripción general del código de inicio

  1. Abre el proyecto con el código de inicio en Android Studio.
  2. 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.
  3. La app no muestra datos de inventario. Observa el BAF para agregar elementos nuevos a la base de datos.
  4. Haz clic en él. La app navega a una pantalla nueva en la que puedes ingresar los detalles del elemento nuevo.

9c5e361a89453821.png

Problemas con el código de inicio

  1. En la pantalla Add Item, ingresa los detalles de un elemento. Presiona Guardar. El fragmento de Add Item se cierra, y el usuario regresa al fragmento anterior. El nuevo elemento no se guarda y no aparece en la pantalla del inventario. Ten en cuenta que la app está incompleta y no se implementa la funcionalidad del botón Save.

f0931dab5089a14f.png

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.

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.

33a193a68c9a8e0e.png

Agrega bibliotecas Room

En esta tarea, agregarás las bibliotecas de componentes Room requeridas a tus archivos de Gradle.

  1. Abre el archivo de nivel de módulo de Gradle, build.gradle (Module: InventoryApp.app). En el bloque dependencies, 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"

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.

8c9f1659ee82ca43.png

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
  1. Abre el código de inicio en Android Studio.
  2. Crea un paquete llamado data en el paquete base com.example.inventory.

be39b42484ba2664.png

  1. En el paquete data, crea una nueva clase de Kotlin llamada Item. 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.
  2. Actualiza la definición de clase Item con el siguiente código. Declara id de tipo Int, itemName de tipo String,, itemPrice de tipo Double y quantityInStock de tipo Int como parámetros para el constructor principal. Asigna a id un valor predeterminado de 0. Esta será la clave primaria, un ID para identificar de manera única cada registro o entrada en la tabla Item.
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 o var.
  • Las clases de datos no pueden ser abstract, open, sealed ni inner.

Para obtener más información sobre las clases de datos, consulta la documentación.

  1. Para convertir la clase Item en una clase de datos, puedes agregar al prefijo su definición de clase con la palabra clave data
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Sobre la declaración de clase Item, anota la clase de datos con @Entity. Usa el argumento tableName para asignar el item como el nombre de la tabla de SQLite.
@Entity(tableName = "item")
data class Item(
   ...
)
  1. Para identificar el id como la clave primaria, anota la propiedad id con @PrimaryKey. Establece el parámetro autoGenerate en true para que Room 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,
   ...
)
  1. Anota las propiedades restantes con @ColumnInfo. La anotación ColumnInfo se usa para personalizar la columna asociada con el campo en particular. Por ejemplo, cuando usas el argumento name, 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 usar tableName 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
)

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.

dcef2fc739d704e5.png

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

bb381857d5fba511.png

Ahora, implementa el elemento DAO en tu app:

  1. En el paquete data, crea la clase de Kotlin ItemDao.kt.
  2. Cambia la definición de clase a interface y anótala con @Dao.
@Dao
interface ItemDao {
}
  1. Dentro del cuerpo de la interfaz, agrega una anotación @Insert. Debajo de @Insert, agrega una función insert() que tome una instancia de la clase Entity 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)
  1. Agrega un argumento OnConflict y asígnale un valor de OnConflictStrategy.IGNORE. El argumento OnConflict le indica a Room qué hacer en caso de conflicto. La estrategia OnConflictStrategy.IGNORE omite un elemento nuevo si 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()).

  1. Agrega una anotación @Update con una función update() para un item. 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 al método insert(), crea el siguiente método update() suspend.
@Update
suspend fun update(item: Item)
  1. Agrega la anotación @Delete con una función delete() 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 buscarlas antes de llamar a la función 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.

  1. 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.
  2. Selecciona todas las columnas de item.
  3. WHERE (id) coincide con un valor específico.

Ejemplo:

SELECT * from item WHERE id = 1
  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ámetro String a @Query, que es una consulta de SQLite para recuperar un elemento de la tabla correspondiente.
  2. Selecciona todas las columnas de item.
  3. 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")
  1. Debajo de la anotación @Query, agrega getItem(), que toma un argumento Int y muestra una Flow<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.

  1. Agrega una @Query con una función getItems():
  2. Haz que la consulta de SQLite muestre todas las columnas de la tabla item, ordenadas de forma ascendente.
  3. Haz que getItems() muestre una lista de entidades Item como Flow. Room mantiene este Flow 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>>
  1. Si bien no verás ningún cambio visible, ejecuta la app para asegurarte de que no haya errores.

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 extienda RoomDatabase. La nueva clase abstracta que definiste actúa como un contenedor de la base de datos. La clase que definiste es abstracta porque Room crea la implementación por ti.
  • Anota la clase con @Database. En los argumentos, enumera las entidades para la base de datos y establece el número de versión.
  • Define una propiedad o un método abstracto que muestre una instancia de ItemDao, y Room generará la implementación por ti.
  • Solo necesitas una instancia de RoomDatabase para toda la app, así que haz que RoomDatabase sea un singleton.
  • Usa el Room.databaseBuilder de Room para crear tu base de datos (item_database), solo si no existe. De lo contrario, muestra la base de datos existente.

Crea la base de datos

  1. En el paquete data, crea una clase de Kotlin ItemRoomDatabase.kt.
  2. En el archivo ItemRoomDatabase.kt, crea la clase ItemRoomDatabase como una clase abstract que extienda RoomDatabase. Anota la clase con @Database. En el siguiente paso, corregirás el error de parámetros faltantes.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. 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 de entities.
  • Establece version como 1. 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 como false para que no se conserven las copias de seguridad del historial de versiones de esquemas.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. 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
  1. 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 {}
  1. Dentro del objeto companion, declara una variable anulable privada INSTANCE para la base de datos y, luego, inicializala en null. La variable INSTANCE 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
  1. Debajo de INSTANCE, mientras estás dentro del objeto companion, define un método getDatabase() con un parámetro Context que necesite el compilador de bases de datos. Muestra un tipo ItemRoomDatabase. Verás un error porque getDatabase() todavía no muestra nada.
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. 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) { }
  1. 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()
  1. Al final del bloque synchronized, muestra instance.
return instance
  1. Dentro del bloque synchronized, inicializa la variable instance y usa el compilador de bases de datos para obtener una base de datos. Pasa a Room.databaseBuilder() el contexto de la aplicación, la clase de la base de datos y un nombre para la base de datos, 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.

  1. 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()
  1. Para crear la instancia de base de datos, llama a .build(). Esto debería quitar los errores de Android Studio.
.build()
  1. Dentro del bloque synchronized, asigna INSTANCE = instance.
INSTANCE = instance
  1. Al final del bloque synchronized, muestra instance. 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
           }
       }
   }
}
  1. 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.

  1. Abre InventoryApplication.kt y crea un val llamado database del tipo ItemRoomDatabase. Para crear una instancia de la instancia database, llama a getDatabase() en ItemRoomDatabase y pasa el contexto. Usa el delegado lazy para que la instancia database 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.

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.

91298a7c05e4f5e0.png

Crea el ViewModel de Inventory

  1. En el paquete com.example.inventory, crea un archivo de clase InventoryViewModel.kt de Kotlin.
  2. Extiende la clase InventoryViewModel desde ViewModel. Pasa el objeto ItemDao como parámetro al constructor predeterminado.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. Al final del archivo InventoryViewModel.kt fuera de la clase, agrega la clase InventoryViewModelFactory para crear una instancia de InventoryViewModel. Pasa el mismo parámetro de constructor que el InventoryViewModel de la instancia ItemDao. Extiende la clase de la clase ViewModelProvider.Factory. Corregirás el error relacionado con los métodos no implementados en el siguiente paso.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. 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 objeto ViewModel.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. Implementa el método create(). Verifica si modelClass es igual a la clase InventoryViewModel 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
)

85c644aced4198c5.png

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.

  1. En la clase InventoryViewModel, agrega una función private llamada insertItem() que reciba un objeto Item y agregue los datos a la base de datos sin bloqueos.
private fun insertItem(item: Item) {
}
  1. 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(), usa viewModelScope.launch para iniciar una corrutina en el ViewModelScope. Dentro de la función de inicio, llama a la función de suspensión insert() en itemDao y pasa el item. ViewModelScope es una propiedad de extensión de la clase ViewModel que cancela automáticamente sus corrutinas secundarias cuando se destruye el ViewModel.
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.

  1. En la clase InventoryViewModel, agrega otra función privada que tome tres strings y muestre una instancia de Item.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Dentro de la clase InventoryViewModel, agrega una función pública llamada addNewItem() que contenga tres strings para los detalles del elemento. Pasa strings de detalles del elemento a la función getNewItemEntry() y asigna el valor que se muestra a un valor llamado newItem. Llama a insertItem() y pasa insertItem 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.

  1. En AddItemFragment.kt, al comienzo de la clase AddItemFragment, crea un private val llamado viewModel del tipo InventoryViewModel. Usa el delegado de propiedades by activityViewModels() de Kotlin para compartir ViewModel en todos los fragmentos. Solucionarás el error en el siguiente paso.
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. Dentro de la expresión lambda, llama al constructor InventoryViewModelFactory() y pasa la instancia ItemDao. Usa la instancia database que creaste en una de las tareas anteriores para llamar al constructor itemDao.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. Debajo de la definición viewModel, crea un lateinit var llamado item del tipo Item.
 lateinit var item: Item
  1. 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 clase InventoryViewModel, agrega la siguiente función public llamada isEntryValid().
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. En AddItemFragment.kt, debajo de la función onCreateView(), crea una función private llamada isEntryValid() que muestre un Boolean. Corregirás el error que indica el valor de devolución que falta en el siguiente paso.
private fun isEntryValid(): Boolean {
}
  1. En la clase AddItemFragment, implementa la función isEntryValid(). Llama a la función isEntryValid() en la instancia viewModel y pasa el texto de las vistas de texto. Muestra el valor de la función viewModel.isEntryValid().
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. En la clase AddItemFragment, debajo de la función isEntryValid(), agrega otra función private llamada addNewItem() sin parámetros y que no muestra nada. Dentro de la función, llama a isEntryValid() dentro de la condición if.
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. Dentro del bloque if, llama al método addNewItem() en la instancia viewModel. En los detalles del elemento que ingresa el usuario, utiliza la instancia binding para leerlos.
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. Debajo del bloque if, crea un val action para volver a ItemListFragment. Llama a findNavController().navigate() y pasa la action.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

Importa androidx.navigation.fragment.findNavController.

  1. 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)
       }
   }
}
  1. Para vincular todo, agrega un controlador de clics al botón Save. En la clase AddItemFragment, sobre la función onDestroyView(), anula onViewCreated().
  2. Dentro de la función onViewCreated(), agrega un controlador de clics al botón Save y llama a addNewItem() desde él.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. Compila y ejecuta la app. Presiona el BAF +. En la pantalla Add Item, agrega los detalles del artículo 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.

193c7fa9c41e0819.png

Consulta la base de datos con el Inspector de bases de datos

  1. 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.
  2. En Android Studio, selecciona View > Tool Windows > Database Inspector en la barra de menú.
  3. En el panel del Inspector de bases de datos, selecciona com.example.inventory en el menú desplegable.
  4. 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.
  5. 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.

4803c08f94e34118.png

¡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!

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

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

5b0a76c50478a73f.png

  1. 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.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. 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

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

21f3eec988dcfbe9.png

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 11c34fc5e516fb1c.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.
  • 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.

Documentación para desarrolladores de Android

Entradas de blog

Videos

Otros artículos y documentación