Cómo conservar datos con Room

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.

La capa de datos contiene repositorios y fuentes de datos

Requisitos previos

  • Saber compilar una interfaz de usuario (IU) básica de una app para Android con Jetpack Compose
  • Saber usar elementos componibles como Text, Icon, IconButton y LazyColumn
  • Saber usar el elemento componible NavHost para definir rutas y pantallas en tu app
  • Saber navegar entre pantallas con un NavHostController
  • Conocer el componente de la arquitectura de Android ViewModel y saber usar ViewModelProvider.Factory para crear una instancia de ViewModels
  • Conocer los conceptos básicos de simultaneidad
  • Saber usar corrutinas para tareas de larga duración
  • Conocimientos básicos sobre las bases de datos SQLite y el lenguaje SQL

Qué aprenderás

  • Cómo crear la base de datos SQLite y cómo interactuar con ella mediante la biblioteca Room
  • Cómo crear una entidad, un objeto de acceso a datos (DAO) y clases de bases de datos
  • Cómo usar un DAO para asignar funciones de Kotlin a consultas en SQL

Qué compilarás

  • Compilarás una app de Inventory que guarde elementos de inventario en la base de datos SQLite.

Qué necesitas

  • 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.

Pantalla de teléfono con elementos del inventario

Pantalla Add Item que aparece en la pantalla del teléfono

Pantalla Add Item con detalles del elemento completados

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:

Descargar ZIP

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

  1. Abre el proyecto con el código de partida 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 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.
  1. Observa que la app no muestra datos de inventario.
  2. 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.

Inventario vacío de la pantalla del teléfono

Pantalla Add Item que aparece en la pantalla del teléfono

Problemas con el código de partida

  1. En la pantalla Add Item, ingresa los detalles de un elemento, como el nombre, el precio y la cantidad.
  2. 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.

Pantalla Add Item con detalles del elemento completados

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.

Pantalla de teléfono con elementos del inventario

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.

Pantalla Add Item con detalles del elemento completados

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.

a3288e8f37250031.png

Agrega dependencias de Room

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

  1. Abre el archivo de Gradle de nivel de módulo build.gradle.kts (Module: InventoryApp.app).
  2. 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.

8c9f1659ee82ca43.png

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.

  1. Abre el código de partida en Android Studio.
  2. Abre el paquete data en el paquete base com.example.inventory.
  3. Dentro del paquete data, abre la clase de Kotlin Item, que representa una entidad de base de datos en tu app.
// No need to copy over, this is part of the starter code
class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)

Clases de datos

Las clases de datos se usan principalmente para conservar datos en Kotlin. Se definen con la palabra clave data. Los objetos de clase de datos de Kotlin tienen algunos beneficios adicionales. Por ejemplo, el compilador genera automáticamente utilidades para comparar, imprimir y copiar elementos como toString(), copy() y equals().

Ejemplo:

// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}

Para garantizar la coherencia y el comportamiento significativo del código generado, las clases de datos deben cumplir con los siguientes requisitos:

  • El constructor principal debe tener al menos un parámetro.
  • Todos los parámetros del constructor principal deben ser val o var.
  • Las clases de datos no pueden ser abstract, open ni sealed.

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

  1. Prefija la definición de la clase Item con la palabra clave data para convertirla en una clase de datos.
data class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)
  1. Sobre la declaración de clase Item, anota la clase de datos con @Entity. Usa el argumento tableName para establecer items como el nombre de la tabla de SQLite.
import androidx.room.Entity

@Entity(tableName = "items")
data class Item(
   ...
)
  1. Anota la propiedad id con @PrimaryKey para que id sea la clave primaria. Una clave primaria es un ID para identificar de manera única cada registro o entrada en la tabla Item.
import androidx.room.PrimaryKey

@Entity(tableName = "items")
data class Item(
    @PrimaryKey
    val id: Int,
    ...
)
  1. Asigna a id un valor predeterminado de 0, que es necesario para que id genere automáticamente valores de id.
  2. Agrega el parámetro autoGenerate a la anotación @PrimaryKey para especificar si la columna de clave primaria se debe generar de forma automática. Si autoGenerate está configurado como true, 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.

8b91b8bbd7256a63.png

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

59aaa051e6a22e79.png

Completa los siguientes pasos para implementar el elemento DAO en tu app:

  1. En el paquete data, crea la interfaz de Kotlin ItemDao.kt.

Campo de nombre completado como elemento DAO

  1. Anota la interfaz ItemDao con @Dao.
import androidx.room.Dao

@Dao
interface ItemDao {
}
  1. Dentro del cuerpo de la interfaz, agrega una anotación @Insert.
  2. Debajo de @Insert, agrega una función insert() que tome una instancia del item de la clase Entity como su argumento.
  3. Marca la función con la palabra clave suspend para permitir que se ejecute en un subproceso separado.

Las operaciones de la base de datos pueden demorar mucho tiempo en ejecutarse, por lo que deben hacerlo en un subproceso independiente. Room no permite el acceso a la base de datos en el subproceso principal.

import androidx.room.Insert

@Insert
suspend fun insert(item: Item)

Cuando se insertan elementos en la base de datos, se pueden generar conflictos. Por ejemplo, varios lugares en el código intentan actualizar la entidad con valores diferentes, en conflicto, como la misma clave primaria. Una entidad es una fila en DB. En la app de Inventory, solo insertamos la entidad desde un lugar que es la pantalla Agregar elemento, por lo que no esperamos que haya ningún conflicto y podemos establecer la estrategia de conflicto como Ignorar.

  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 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.

  1. Agrega una función nueva con la anotación @Update que tome un Item como parámetro.

La entidad que se actualiza tiene la misma clave primaria que la que se pasa. Puedes actualizar algunas o todas las demás propiedades de la entidad.

  1. Al igual que con el método insert(), marca esta función con la palabra clave suspend.
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.

  1. Escribe una consulta de SQLite para recuperar un elemento específico de la tabla de elementos según el id especificado. El siguiente código proporciona una consulta de muestra que selecciona todas las columnas de items, donde id coincide con un valor específico y id es un identificador único.

Ejemplo:

// Example, no need to copy over
SELECT * from items WHERE id = 1
  1. Agrega una anotación @Query.
  2. Usa la consulta de SQLite del paso anterior como un parámetro de cadena a la anotación @Query.
  3. Agrega un parámetro String a @Query, que es una consulta de SQLite para recuperar un elemento de la tabla correspondiente.

La consulta ahora indica que se seleccionen todas las columnas de items, donde id coincide con el argumento :id. Observa que :id usa la notación de dos puntos en la consulta para hacer referencia a argumentos en la función.

@Query("SELECT * from items WHERE id = :id")
  1. Después de la anotación @Query, agrega una función getItem() que tome un argumento Int y muestre un Flow<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.

  1. Agrega una @Query con una función getAllItems().
  2. Haz que la consulta de SQLite muestre todas las columnas de la tabla item, ordenadas de forma ascendente.
  3. Haz que getAllItems() 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 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>>
}
  1. Si bien no notarás ningún cambio visible, compila la app para asegurarte de que no tenga errores.

7. Crea una instancia de base de datos

En esta tarea, crearás una RoomDatabase que use tu Entity y tu DAO a partir de las tareas anteriores. La clase de base de datos define la lista de entidades y DAO.

La clase Database proporciona a tu app instancias de los DAO que definas. A su vez, la app puede usar los DAO para recuperar datos de la base de datos como instancias de objetos de entidad de datos asociados. La app también puede usar las entidades de datos definidas para actualizar filas de las tablas correspondientes o crear filas nuevas para su inserción.

Debes crear una clase abstracta RoomDatabase y anotarla con @Database. Esta clase tiene un método que muestra la instancia existente de RoomDatabase si la base de datos no existe.

Este es el proceso general para obtener la instancia RoomDatabase:

  • Crea una clase public abstract que extienda RoomDatabase. La nueva clase abstracta que defines actúa como un contenedor de la base de datos. La clase que defines es abstracta porque Room crea la implementación por ti.
  • Anota la clase con @Database. En los argumentos, enumera las entidades para la base de datos y establece el número de versión.
  • Define una propiedad o un método abstracto que muestre una instancia de ItemDao, y Room genera la implementación por ti.
  • Solo necesitas una instancia de RoomDatabase para toda la app, así que haz que RoomDatabase sea un singleton.
  • Usa el Room.databaseBuilder de Room para crear tu base de datos (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 InventoryDatabase.kt.
  2. En el archivo InventoryDatabase.kt, haz que la clase InventoryDatabase sea una clase abstract que extienda RoomDatabase.
  3. 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.

  1. Especifica el Item como la única clase con la lista de entities.
  2. Establece version como 1. Cada vez que cambies el esquema de la tabla de la base de datos, debes aumentar el número de versión.
  3. 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. Dentro del cuerpo de la clase, declara una función abstracta que muestre el ItemDao de modo que la base de datos sepa sobre el DAO.
abstract fun itemDao(): ItemDao
  1. 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 {}
  1. Dentro del objeto companion, declara una variable anulable privada Instance para la base de datos y, luego, inicialízala en null.

La variable Instance conserva una referencia a la base de datos, cuando se crea una. Esto ayuda a mantener una sola instancia de la base de datos abierta en un momento determinado, que es un recurso costoso para crear y mantener.

  1. Anota Instance con @Volatile.

El valor de una variable volátil nun