Cómo leer y actualizar datos con Room

1. Antes de comenzar

En los codelabs anteriores, aprendiste a usar una biblioteca de persistencias Room, una capa de abstracción sobre una base de datos SQLite para almacenar datos de app. En este codelab, agregarás más funciones a la app de "Inventory" (inventario) y aprenderás a leer, mostrar, actualizar y borrar datos de la base de datos SQLite usando Room. Usarás una RecyclerView para mostrar los datos de la base de datos y actualizarlos automáticamente cuando se modifiquen los datos subyacentes en la base de datos.

Requisitos previos

  • Debes saber crear una base de datos SQLite e interactuar con ella usando la biblioteca de Room.
  • Debes saber crear una entidad, un DAO y clases de bases de datos.
  • Debes saber usar un objeto de acceso a datos (DAO) para asignar funciones de Kotlin a consultas de SQL.
  • Debes saber mostrar los elementos de lista en una RecyclerView.
  • Debes haber completado el codelab anterior de esta unidad: Cómo conservar datos con Room.

Qué aprenderás

  • Cómo leer y mostrar entidades de una base de datos SQLite
  • Cómo actualizar y borrar entidades de una base de datos SQLite usando la biblioteca de Room

Qué compilarás

  • Compilarás una app de Inventory que muestra una lista de elementos de inventario. La app puede actualizar, editar y borrar elementos de la base de datos de la app usando Room.

2. Descripción general de la app de inicio

Este codelab usa el código de solución de la app de Inventory del codelab anterior como código de partida. La app de partida ya guarda datos con la biblioteca de persistencias Room. El usuario puede agregar datos a la base de datos de la app usando la pantalla Add Item.

Nota: La versión actual de la app de partida no muestra la fecha almacenada en la base de datos.

771c6a677ecd96c7.png

En este codelab, extenderás la app para leer y mostrar los datos, actualizar y borrar entidades en la base de datos usando la biblioteca Room.

Descarga el código de partida para este codelab

Este código de partida es el mismo que el código de solución del codelab anterior.

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.

3. Agrega una RecyclerView

En esta tarea, agregarás una RecyclerView a la app para mostrar los datos almacenados en la base de datos.

Agrega una función auxiliar para darle formato al precio

A continuación, se incluye una captura de pantalla de la app final.

d6e7b7b9f12e7a16.png

Ten en cuenta que el precio se muestra en el formato de moneda. Para convertir un valor doble al formato de moneda deseado, agrega una función de extensión a la clase Item.

Funciones de extensión

Kotlin permite extender una clase con funcionalidades nuevas sin tener que heredar contenido de la clase ni modificar su definición existente. Eso significa que puedes agregar funciones a una clase existente sin necesidad de acceder a su código fuente. Esto se hace mediante declaraciones especiales llamadas extensiones.

Por ejemplo, puedes escribir nuevas funciones para una clase desde una biblioteca de terceros que no puedas modificar. Esas funciones están disponibles para realizar llamadas de la forma habitual, como si fueran métodos de la clase original. Se las conoce como funciones de extensión. (También hay propiedades de extensión que te permiten definir propiedades nuevas para clases existentes, pero están fuera del alcance de este codelab).

Las funciones de extensión en realidad no modifican la clase, pero te permiten usar la notación de puntos cuando llamas a la función en objetos de esa clase.

Por ejemplo, en el siguiente fragmento de código, tienes una clase llamada Square, que tiene una propiedad para el lado y una función para calcular el área del cuadrado. Observa la función de extensión Square.perimeter(): el nombre de la función tiene el prefijo de la clase en la que opera. Dentro de la función, puedes hacer referencia a las propiedades públicas de la clase Square.

Observa el uso de la función de extensión en la función main(). Se llama a la función de extensión creada, perimeter(), como una función normal dentro de esa clase Square.

Ejemplo:

class Square(val side: Double){
        fun area(): Double{
        return side * side;
    }
}

// Extension function to calculate the perimeter of the square
fun Square.perimeter(): Double{
        return 4 * side;
}

// Usage
fun main(args: Array<String>){
      val square = Square(5.5);
      val perimeterValue = square.perimeter()
      println("Perimeter: $perimeterValue")
      val areaValue = square.area()
      println("Area: $areaValue")
}

En este paso, darás formato al precio del artículo como una string de formato de moneda. En general, no es conveniente cambiar una clase de entidad que represente datos solo para darles formato (consulta el principio de responsabilidad única), por lo que agregarás una función de extensión.

  1. En Item.kt, debajo de la definición de clase, agrega una función de extensión llamada Item.getFormattedPrice() que no reciba parámetros y muestre una String. Observa el nombre de la clase y la notación de puntos en el nombre de la función.
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

Cuando Android Studio te lo solicite, importa java.text.NumberFormat.

Agrega ListAdapter

En este paso, agregarás un adaptador de lista a RecyclerView. Como ya estás familiarizado con la implementación del adaptador por haber trabajado en los codelabs anteriores, resumimos las instrucciones a continuación. El archivo ItemListAdapter completo se encuentra al final de este paso para tu comodidad y para ayudarte a comprender los conceptos de Room en el codelab.

  1. En el paquete com.example.inventory, agrega una clase de Kotlin llamada ItemListAdapter. Pasa una función llamada onItemClicked() como un parámetro de constructor que tome un objeto Item como parámetro.
  2. Cambia la firma de la clase ItemListAdapter para extender ListAdapter. Pasa Item y ItemListAdapter.ItemViewHolder como parámetros.
  3. Agrega el parámetro constructor DiffCallback; ListAdapter lo usará para averiguar qué cambió en la lista.
  4. Anula los métodos obligatorios onCreateViewHolder() y onBindViewHolder().
  5. El método onCreateViewHolder() muestra un ViewHolder nuevo cuando RecyclerView lo necesita.
  6. Dentro del método onCreateViewHolder(), crea un nuevo objeto View y auméntalo desde el archivo de diseño item_list_item.xml usando la clase de vinculación generada automáticamente, ItemListItemBinding.
  7. Implementa el método onBindViewHolder(). Obtén el elemento actual con el método getItem(), pasando la posición.
  8. Configura el objeto de escucha de clics en el objeto itemView y llama a la función onItemClicked() dentro del objeto de escucha.
  9. Define la clase ItemViewHolder y extiende la función desde RecyclerView.ViewHolder. Anula la función bind() y pasa el objeto Item.
  10. Define un objeto complementario. Dentro del objeto complementario, define un val del tipo DiffUtil.ItemCallback<Item>() llamado DiffCallback. Anula los métodos obligatorios areItemsTheSame() y areContentsTheSame(), y defínelos.

La clase finalizada debería verse de la siguiente manera:

package com.example.inventory

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.inventory.data.Item
import com.example.inventory.data.getFormattedPrice
import com.example.inventory.databinding.ItemListItemBinding

/**
* [ListAdapter] implementation for the recyclerview.
*/

class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
   ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
       return ItemViewHolder(
           ItemListItemBinding.inflate(
               LayoutInflater.from(
                   parent.context
               )
           )
       )
   }

   override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
       val current = getItem(position)
       holder.itemView.setOnClickListener {
           onItemClicked(current)
       }
       holder.bind(current)
   }

   class ItemViewHolder(private var binding: ItemListItemBinding) :
       RecyclerView.ViewHolder(binding.root) {

       fun bind(item: Item) {

       }
   }

   companion object {
       private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
           override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem === newItem
           }

           override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem.itemName == newItem.itemName
           }
       }
   }
}

Observa la pantalla de lista de inventario de la app finalizada (la app de solución que se incluye al final de este codelab). Ten en cuenta que todos los elementos de lista muestran el nombre del elemento del inventario, el precio en formato de moneda y el stock a mano. En los pasos anteriores, usaste el archivo de diseño item_list_item.xml con tres objetos TextView para crear filas. En el siguiente paso, vincularás los detalles de la entidad a estos TextViews.

9c416f2fbf1e5ae2.png

  1. En ItemListAdapter.kt, implementa la función bind() en la clase ItemViewHolder. Vincula el objeto TextView itemName a item.itemName. Obtén el precio en formato de moneda con la función de extensión getFormattedPrice() y vincúlalo a la TextView itemPrice. Convierte el valor de quantityInStock en String y vincúlalo a la TextView itemQuantity. El método completo debería verse de la siguiente manera:
fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemQuantity.text = item.quantityInStock.toString()
   }
}

Cuando Android Studio te lo solicite, importa com.example.inventory.data.getFormattedPrice.

Usa ListAdapter

En esta tarea, actualizarás InventoryViewModel y ItemListFragment para mostrar los detalles del elemento en la pantalla con el adaptador de lista que creaste en el paso anterior.

  1. Al comienzo de la clase InventoryViewModel, crea un val llamado allItems del tipo LiveData<List<Item>> para los elementos de la base de datos. No te preocupes por el error, ya que lo solucionarás pronto.
val allItems: LiveData<List<Item>>

Importa androidx.lifecycle.LiveData cuando Android Studio lo solicite.

  1. Llama a getItems() en itemDao y asígnalo a allItems. La función getItems() muestra un Flow. Para consumir los datos como un valor LiveData, usa la función asLiveData(). La definición finalizada debería verse de la siguiente manera:
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

Importa androidx.lifecycle.asLiveData cuando Android Studio lo solicite.

  1. En ItemListFragment, al comienzo de la clase, declara una propiedad private inmutable llamada viewModel del tipo InventoryViewModel. Usa el delegado by para entregar la inicialización de propiedad a la clase activityViewModels. Pasa el constructor InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importa androidx.fragment.app.activityViewModels cuando Android Studio lo solicite.

  1. Aún dentro de ItemListFragment, desplázate hasta la función onViewCreated(). Debajo de la llamada a super.onViewCreated(), declara un val llamado adapter. Inicializa la nueva propiedad adapter con el constructor predeterminado; ItemListAdapter{} no pasa nada.
  2. Vincula el adapter recién creado a recyclerView de la siguiente manera:
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
  1. Dentro de onViewCreated(), después de configurar el adaptador, adjunta un observador en allItems para escuchar los cambios en los datos.
  2. Dentro del observador, llama a submitList() en adapter y pasa la lista nueva. Esto actualizará RecyclerView con los elementos nuevos de la lista.
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
   items.let {
       adapter.submitList(it)
   }
}
  1. Verifica que el método onViewCreated() completado se vea como se muestra a continuación. Ejecuta la app. Ten en cuenta que se muestra la lista de inventario, si hay elementos guardados en la base de datos de tu app. Agrega algunos elementos del inventario a la base de datos si la lista está vacía.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   val adapter = ItemListAdapter {
      }
   binding.recyclerView.adapter = adapter
   viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
       items.let {
           adapter.submitList(it)
       }
   }
   binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
   binding.floatingActionButton.setOnClickListener {
       val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
           getString(R.string.add_fragment_title)
       )
       this.findNavController().navigate(action)
   }
}

9c416f2fbf1e5ae2.png

4. Muestra los detalles del elemento

En esta tarea, leerás y mostrarás los detalles de la entidad en la pantalla Item Details. Usarás la clave primaria (el elemento id) para leer los detalles, como el nombre, el precio y la cantidad, de la base de datos de la app de inventario, y los mostrarás en la pantalla Item Details con el archivo de diseño fragment_item_detail.xml. fragment_item_detail.xml ya está prediseñado y contiene tres objetos TextView que muestran los detalles del elemento.

d699618f5d9437df.png

En esta tarea, realizarás los siguientes pasos:

  • Agregarás un controlador de clics a RecyclerView para navegar por la app a la pantalla Item Details.
  • En el fragmento ItemListFragment, recuperarás los datos de la base de datos y la pantalla.
  • Vincula los objetos TextView con los datos de ViewModel.

Agrega un controlador de clics

  1. En ItemListFragment, desplázate hasta la función onViewCreated() para actualizar la definición del adaptador.
  2. Agrega una lambda como parámetro de constructor a ItemListAdapter{}.
val adapter = ItemListAdapter {
}
  1. Dentro de la expresión lambda, crea un objeto val llamado action. Pronto corregirás el otro error.
val adapter = ItemListAdapter {
    val action
}
  1. Llama al método actionItemListFragmentToItemDetailFragment() en el objeto ItemListFragmentDirections que pasa el elemento id. Asigna a action el objeto NavDirections que se muestra.
val adapter = ItemListAdapter {
   val action =    ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
  1. Debajo de la definición action, recupera una instancia NavController usando this.findNavController() y llama a navigate() cuando se pase la action. La definición del adaptador debería verse así:
val adapter = ItemListAdapter {
   val action =   ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
   this.findNavController().navigate(action)
}
  1. Ejecuta la app. Haz clic en un elemento de RecyclerView. La app navega a la pantalla Item Details. Observa que los detalles están en blanco. Si presiona los botones, no ocurra nada.

196553111ee69beb.png

En pasos posteriores, se mostrarán los detalles de la entidad en la pantalla Item Details y se agregará funcionalidad a los botones para vender y borrar.

Recupera detalles del elemento

En este paso, agregarás una función nueva a InventoryViewModel para recuperar los detalles del elemento de la base de datos según el elemento id. En el siguiente paso, usarás esta función para mostrar los detalles de la entidad en la pantalla Item details.

  1. En InventoryViewModel, agrega una función llamada retrieveItem() que tome una Int para el ID del elemento y muestre un LiveData<Item>. Pronto corregirás el error de la expresión que se muestra.
fun retrieveItem(id: Int): LiveData<Item> {
}
  1. Dentro de la nueva función, llama a getItem() en itemDao y pasa el parámetro id. La función getItem() muestra un Flow. Para consumir el valor de Flow como LiveData, llama a la función asLiveData() y utiliza esto como lo que muestra la función retrieveItem(). La función completada debería verse de la siguiente manera:
fun retrieveItem(id: Int): LiveData<Item> {
   return itemDao.getItem(id).asLiveData()
}

Vincula datos con TextView

En este paso, crearás una instancia de ViewModel en ItemDetailFragment y vincularás los datos de ViewModel a los objetos TextView en la pantalla Item Details. También adjuntarás un observador de los datos en ViewModel para mantener tu lista de inventario actualizada en la pantalla si cambian los datos subyacentes de la base de datos.

  1. En ItemDetailFragment, agrega una propiedad mutable llamada item de la entidad Item de tipo. Usarás esa propiedad para almacenar información sobre una sola entidad. La propiedad se inicializará más tarde, así que debes agregarle el prefijo lateinit.
lateinit var item: Item

Importa com.example.inventory.data.Item cuando Android Studio lo solicite.

  1. Al comienzo de la clase ItemDetailFragment, declara una propiedad inmutable private llamada viewModel del tipo InventoryViewModel. Usa el delegado by para entregar la inicialización de propiedad a la clase activityViewModels. Pasa el constructor InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importa androidx.fragment.app.activityViewModels si Android Studio lo solicita.

  1. Aún en ItemDetailFragment, crea una función private llamada bind() que tome una instancia de la entidad Item como parámetro y no muestre nada.
private fun bind(item: Item) {
}
  1. Implementa la función bind(), que es similar a lo que hiciste en ItemListAdapter. Establece la propiedad text de la TextView itemName en item.itemName. Llama a getFormattedPrice() en la propiedad item para dar formato al valor de precio y establecerlo en la propiedad text de la TextView itemPrice. Convierte el valor quantityInStock en String y configúralo en la propiedad text de la TextView itemQuantity.
private fun bind(item: Item) {
   binding.itemName.text = item.itemName
   binding.itemPrice.text = item.getFormattedPrice()
   binding.itemCount.text = item.quantityInStock.toString()
}
  1. Actualiza la función bind() a fin de usar la función de alcance apply{} para el bloque de código, como se muestra a continuación.
private fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemCount.text = item.quantityInStock.toString()
   }
}
  1. Aún en ItemDetailFragment, anula onViewCreated().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
}
  1. En uno de los pasos anteriores, pasaste el ID de elemento como un argumento de navegación a ItemDetailFragment desde ItemListFragment. Dentro de onViewCreated(), debajo de la llamada a la función de superclase, crea una variable inmutable llamada id. Recupera y asigna el argumento de navegación a esta nueva variable.
val id = navigationArgs.itemId
  1. Ahora, usarás esta variable id para recuperar los detalles del elemento. Aún dentro de onViewCreated(), llama a la función retrieveItem() en el viewModel pasando el id. Adjunta un observador al valor que se muestra pasando el viewLifecycleOwner y una lambda.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. Dentro de la expresión lambda, pasa selectedItem como el parámetro que contiene la entidad Item recuperada de la base de datos. En el cuerpo de la función lambda, asigna el valor selectedItem a item. Llama a la función bind() pasando el item. La función completada debería verse de la siguiente manera:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
       item = selectedItem
       bind(item)
   }
}
  1. Ejecuta la app. Haz clic en cualquier elemento de la lista en la pantalla Inventory, y se mostrará la pantalla Item Details. Observa que la pantalla ya no está en blanco y muestra los detalles de entidad recuperados de la base de datos de inventario.

  1. Presiona los botones Sell, Delete y BAF. No pasa nada. En las tareas siguientes, implementarás la funcionalidad de esos botones.

5. Implementa un artículo de venta

En esta tarea, extenderás las funciones de la app e implementarás la funcionalidad de venta. A continuación, se presenta un resumen de las instrucciones para este paso.

  • Agregar una función en ViewModel para actualizar una entidad
  • Crear un método nuevo para reducir la cantidad y actualizar la entidad en la base de datos de la app
  • Adjuntar un objeto de escucha de clics al botón Sell
  • Inhabilitar el botón Sell si la cantidad es cero

Hagamos lo siguiente:

  1. En InventoryViewModel, agrega una función privada llamada updateItem() que tome una instancia de la clase de entidad Item y no muestre nada.
private fun updateItem(item: Item) {
}
  1. Implementa el nuevo método, updateItem(). Para llamar al método de suspensión update() desde la clase ItemDao, inicia una corrutina con el viewModelScope. Dentro del bloque de inicio, realiza una llamada a la función update() en itemDao pasando el item. El método completado debe verse de la siguiente manera.
private fun updateItem(item: Item) {
   viewModelScope.launch {
       itemDao.update(item)
   }
}
  1. Dentro de InventoryViewModel, agrega otro método llamado sellItem() que tome una instancia de la clase de entidad Item y no muestre nada.
fun sellItem(item: Item) {
}
  1. Dentro de la función sellItem(), agrega una condición if para verificar si item.quantityInStock es mayor que 0.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
   }
}

Dentro del bloque if, usarás la función copy() para que la clase Data actualice la entidad.

Clase de datos: copy()

La función copy() se proporciona de forma predeterminada a todas las instancias de clases de datos. Esta función se usa para copiar un objeto a fin de cambiar algunas de sus propiedades, pero no modificar el resto.

Por ejemplo, la clase User y su instancia jack, como se muestra a continuación. Si quieres crear una instancia nueva y actualizar solo la propiedad age, su implementación será la siguiente:

Ejemplo

// Data class
data class User(val name: String = "", val age: Int = 0)

// Data class instance
val jack = User(name = "Jack", age = 1)

// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
  1. Vuelve a la función sellItem() en InventoryViewModel. Dentro del bloque if, crea una nueva propiedad inmutable llamada newItem. Llama a la función copy() en la instancia item y pasa el quantityInStock actualizado, lo que disminuye la acción en 1.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  1. Debajo de la definición de newItem, haz una llamada a la función updateItem() y pasa la nueva entidad actualizada, que es newItem. El método completo debe verse de la siguiente manera.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
       // Decrease the quantity by 1
       val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
       updateItem(newItem)
   }
}
  1. Para agregar la función de stock para vender, ve a ItemDetailFragment. Desplázate hasta el final de la función bind(). Dentro del bloque apply, establece un objeto de escucha de clics en el botón Sell y llama a la función sellItem() en viewModel.
private fun bind(item: Item) {
binding.apply {

...
    sellItem.setOnClickListener { viewModel.sellItem(item) }
    }
}
  1. Ejecuta la app. En la pantalla Inventory, haz clic en un elemento de la lista con una cantidad mayor que cero. Se mostrará la pantalla Item Details. Presiona el botón Sell y observa que el valor de cantidad disminuye.

aa63ca761dc8f009.png

  1. En la pantalla Item Details, presiona continuamente el botón Sell hasta lograr que la cantidad sea 0. (Sugerencia: Selecciona una entidad con menos acciones o crea una nueva con menos cantidad). Cuando la cantidad sea cero, presiona el botón Sell. No se producirá ningún cambio visual. Eso se debe a que la función sellItem() comprueba si la cantidad es mayor que cero antes de actualizar la cantidad.

3e099d3c55596938.png

  1. Para proporcionar mejores comentarios a los usuarios, puedes inhabilitar el botón Sell cuando no haya ningún artículo para vender. En InventoryViewModel, agrega una función para verificar si la cantidad es mayor que 0. Asigna el nombre isStockAvailable() a la función, que toma una instancia de Item y muestra un Boolean.
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. Ve a ItemDetailFragment y desplázate hasta la función bind(). Dentro del bloque apply, llama a la función isStockAvailable() en viewModel pasando el item. Establece el valor que se muestra en la propiedad isEnabled del botón Sell. El código debería ser similar al siguiente:
private fun bind(item: Item) {
   binding.apply {
       ...
       sellItem.isEnabled = viewModel.isStockAvailable(item)
       sellItem.setOnClickListener { viewModel.sellItem(item) }
   }
}
  1. Ejecuta la app. Observa que el botón Sell está inhabilitado cuando la cantidad en stock es cero. Felicitaciones por implementar la función de venta de artículos en tu app.

5e49db8451e77c2b.png

Entidad Delete item

De manera similar a la tarea anterior, extenderás las funciones de tu app implementando la funcionalidad de eliminación. Estas son las instrucciones de alto nivel para este paso que es mucho más fácil que implementar la función de venta.

  • Agregar una función en ViewModel para borrar una entidad de la base de datos
  • Agregar un nuevo método en ItemDetailFragment para llamar a la nueva función de borrar y administrar la navegación
  • Adjuntar un objeto de escucha de clics al botón Delete

Continuemos con el código:

  1. En InventoryViewModel, agrega una nueva función llamada deleteItem(), que toma una instancia de la clase de entidad Item llamada item y no muestra nada. Dentro de la función deleteItem(), inicia una corrutina con viewModelScope. Dentro del bloque launch, llama al método delete() en itemDao pasando el item.
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. En ItemDetailFragment, desplázate hasta el inicio de la función deleteItem(). Llama a deleteItem() en el viewModel y pasa item. La instancia item contiene la entidad que se muestra actualmente en la pantalla Item Details. El método completo debería verse de la siguiente manera:
private fun deleteItem() {
   viewModel.deleteItem(item)
   findNavController().navigateUp()
}
  1. Aún dentro de ItemDetailFragment, desplázate hasta la función showConfirmationDialog(). Se proporciona esta función como parte del código de partida. Este método muestra un cuadro de diálogo de alerta para obtener la confirmación del usuario antes de borrar el elemento y llama a la función deleteItem() cuando se presiona el botón positivo.
private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            ...
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

La función showConfirmationDialog() muestra un diálogo de alerta similar al siguiente:

728bfcbb997c8017.png

  1. En ItemDetailFragment, al final de la función bind(), dentro del bloque apply, configura el objeto de escucha de clics en el botón para borrar. Llama a showConfirmationDialog() dentro de la lambda del objeto de escucha click.
private fun bind(item: Item) {
   binding.apply {
       ...
       deleteItem.setOnClickListener { showConfirmationDialog() }
   }
}
  1. Ejecuta la app. Selecciona un elemento de lista en la pantalla de lista Inventory, en la pantalla Item Details y presiona el botón Delete. Presiona Yes, y la app navegará hacia atrás a la pantalla Inventory. Observa que la entidad que borraste ya no está en la base de datos de la app. Felicitaciones por implementar la función de borrar.

c05318ab8c216fa1.png

Entidad Edit item

De manera similar a las tareas anteriores, en esta agregarás otra mejora de la función en la app. Implementarás la entidad edit item.

A continuación, te mostramos los pasos rápidos para editar una entidad en la base de datos de la app:

  • Para reutilizar la pantalla Add Item, actualiza el título del fragmento a Edit Item.
  • Agrega un objeto de escucha de clics al BAF para navegar a la pantalla Edit Item.
  • Propaga los objetos TextView con los detalles de la entidad.
  • Actualiza la entidad en la base de datos con Room.

Cómo agregar objetos de escucha de clics al BAF

  1. En ItemDetailFragment, agrega una nueva función private llamada editItem() que no tome parámetros ni muestre nada. En el siguiente paso, volverás a usar fragment_add_item.xml para actualizar el título de la pantalla a Edit Item. Para lograrlo, enviarás la string del título del fragmento junto con el ID del elemento como parte de la acción.
private fun editItem() {
}

Después de actualizar el título del fragmento, la pantalla Edit Item debería verse de la siguiente manera.

bcd407af7c515a21.png

  1. Dentro de la función editItem(), crea una variable inmutable llamada action. Realiza una llamada a actionItemDetailFragmentToAddItemFragment() en ItemDetailFragmentDirections pasando la string del título, edit_fragment_title y el elemento id. Asigna el valor mostrado a action. Debajo de la definición de action, llama a this.findNavController().navigate() y pasa action para navegar a la pantalla Edit Item.
private fun editItem() {
   val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
       getString(R.string.edit_fragment_title),
       item.id
   )
   this.findNavController().navigate(action)
}
  1. Aún en ItemDetailFragment, desplázate hasta la función bind(). Dentro del bloque apply, establece el objeto de escucha de clics en el BAF y llama a la función editItem() desde la lambda para navegar a la pantalla Edit Item.
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. Ejecuta la app. Ve a la pantalla Item Details. Haz clic en el BAF. Observa que el título de la pantalla se actualiza a Edit Item, pero todos los campos de texto están vacíos. En el siguiente paso, solucionarás el problema.

a6a6583171b68230.png

Cómo propagar objetos TextView

En este paso, completarás los campos de texto en la pantalla Edit Item con los detalles de la entidad. Dado que usamos la pantalla Add Item, agregarás nuevas funciones al archivo Kotlin, AddItemFragment.kt.

  1. En AddItemFragment, agrega una nueva función private para vincular los campos de texto con los detalles de la entidad. Asigna el nombre bind() a la función que toma instancias de la clase de entidad Item y no muestra ningún resultado.
private fun bind(item: Item) {
}
  1. La implementación de la función bind() es muy similar a la que realizaste antes en ItemDetailFragment. Dentro de la función bind(), redondea el precio a dos lugares decimales con la función format() y asígnalo a un val llamado price, como se muestra a continuación.
val price = "%.2f".format(item.itemPrice)
  1. Debajo de la definición price, usa la función de permiso apply en la propiedad binding, como se muestra a continuación.
binding.apply {
}
  1. Dentro del bloque de código de la función de alcance apply, establece item.itemName en la propiedad de texto de itemName. Usa la función setText() y pasa la string item.itemName y TextView.BufferType.SPANNABLE como BufferType.
binding.apply {
   itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}

Importa android.widget.TextView si Android Studio lo solicita.

  1. Al igual que en el paso anterior, establece la propiedad de texto del precio EditText, como se muestra a continuación. Para establecer la propiedad text de la cantidad de EditText, recuerda convertir el item.quantityInStock en String. La función completada debería verse así.
private fun bind(item: Item) {
   val price = "%.2f".format(item.itemPrice)
   binding.apply {
       itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
       itemPrice.setText(price, TextView.BufferType.SPANNABLE)
       itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
   }
}
  1. Dentro del AddItemFragment, desplázate hasta la función onViewCreated(). Después de la llamada a la función superclase, crea un val llamado id y recupera itemId de los argumentos de navegación.
val id = navigationArgs.itemId
  1. Agrega un bloque if-else con una condición para verificar si id es mayor que cero y mueve al bloque else el objeto de escucha del botón Save. Dentro del bloque if, se recupera la entidad con id y se agrega un observador. Dentro del observador, actualiza la propiedad item y llama a bind() pasando el item. Te proporcionamos la función completa para que la copies y pegues. Es fácil de entender. Solo tienes que descifrarlo por tu cuenta.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   if (id > 0) {
       viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
           item = selectedItem
           bind(item)
       }
   } else {
       binding.saveAction.setOnClickListener {
           addNewItem()
       }
   }
}
  1. Ejecuta la aplicación. Ve a Item Details y presiona el BAF +. Observa que los campos están completos con los detalles del artículo. Edita la cantidad en stock o cualquier otro campo, y presiona el botón para guardar. No pasa nada. Eso se debe a que no estás actualizando la entidad en la base de datos de la app. Podrás solucionar este problema pronto.

829ceb9dd7993215.png

Actualiza la entidad con Room

En esta tarea final, agregarás las partes finales del código para implementar la funcionalidad de actualización. Definirás las funciones necesarias en el ViewModel y las usarás en el AddItemFragment.

¡A codificar de nuevo!

  1. En InventoryViewModel, agrega una función private llamada getUpdatedItemEntry() que se tome en Int y tres strings para los detalles de la entidad denominadas itemName, itemPrice y itemCount. Muestra una instancia de Item de la función. El código se proporciona como referencia.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
}
  1. Dentro de la función getUpdatedItemEntry(), crea una instancia de elemento con los parámetros de la función, como se muestra a continuación. Muestra la instancia Item de la función.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
   return Item(
       id = itemId,
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Dentro del InventoryViewModel, agrega otra función llamada updateItem(). Esta función también toma un Int y tres strings para los detalles de la entidad, y no muestra ningún resultado. Usa los nombres de variables del siguiente fragmento de código.
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
}
  1. Dentro de la función updateItem(), realiza una llamada a la función getUpdatedItemEntry() pasando la información de la entidad, que se pasa como parámetros de función, como se muestra a continuación. Asigna el valor que se muestra a una variable inmutable llamada updatedItem.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
  1. Justo debajo de la llamada a la función getUpdatedItemEntry(), realiza una llamada a la función updateItem() pasando el updatedItem. La función completada se ve de la siguiente manera:
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
   val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
   updateItem(updatedItem)
}
  1. Regresa a AddItemFragment y agrega una función privada llamada updateItem() sin parámetros y que no muestre nada. Dentro de la función, agrega una condición if para validar la entrada del usuario llamando a la función isEntryValid().
private fun updateItem() {
   if (isEntryValid()) {
   }
}
  1. Dentro del bloque if, realiza una llamada a viewModel.updateItem() pasando los detalles de la entidad. Usa el itemId de los argumentos de navegación y los demás detalles de la entidad, como el nombre, el precio y la cantidad, de los EditText, como se muestra a continuación.
viewModel.updateItem(
    this.navigationArgs.itemId,
    this.binding.itemName.text.toString(),
    this.binding.itemPrice.text.toString(),
    this.binding.itemCount.text.toString()
)
  1. Debajo de la llamada a función updateItem(), define un elemento val llamado action. Llama a actionAddItemFragmentToItemListFragment() en AddItemFragmentDirections y asigna el valor que se muestra a action. Navega a ItemListFragment, llama al findNavController().navigate() y pasa action.
private fun updateItem() {
   if (isEntryValid()) {
       viewModel.updateItem(
           this.navigationArgs.itemId,
           this.binding.itemName.text.toString(),
           this.binding.itemPrice.text.toString(),
           this.binding.itemCount.text.toString()
       )
       val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
       findNavController().navigate(action)
   }
}
  1. Aún dentro de AddItemFragment, desplázate hasta la función bind(). Dentro del bloque de la función de alcance binding.apply, establece el objeto de escucha de clics para el botón Save. Realiza una llamada a la función updateItem() dentro de la lambda, como se muestra a continuación.
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. Ejecuta la app. Intenta editar elementos de inventario. Deberías poder editar cualquier elemento de la base de datos de la app de Inventory.

1bbd094a77c25fc4.png

Felicitaciones por crear tu primera app y usar Room para administrar la base de datos.

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

  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.

7. Resumen

  • Kotlin permite extender una clase con funcionalidades nuevas sin tener que heredar contenido de la clase ni modificar su definición existente. Esto se hace mediante declaraciones especiales llamadas extensiones.
  • Para consumir los datos de Flow como un valor de LiveData, usa la función asLiveData().
  • La función copy() se proporciona de forma predeterminada a todas las instancias de clases de datos. Te permite copiar un objeto y cambiar algunas de sus propiedades, sin modificar el resto de sus propiedades.

8. Más información

Documentación para desarrolladores de Android

Referencias de la API

Referencias de Kotlin