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.
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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se 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.
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.
- En
Item.kt
, debajo de la definición de clase, agrega una función de extensión llamadaItem.getFormattedPrice()
que no reciba parámetros y muestre unaString
. 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.
- En el paquete
com.example.inventory
, agrega una clase de Kotlin llamadaItemListAdapter
. Pasa una función llamadaonItemClicked()
como un parámetro de constructor que tome un objetoItem
como parámetro. - Cambia la firma de la clase
ItemListAdapter
para extenderListAdapter
. PasaItem
yItemListAdapter.ItemViewHolder
como parámetros. - Agrega el parámetro constructor
DiffCallback
;ListAdapter
lo usará para averiguar qué cambió en la lista. - Anula los métodos obligatorios
onCreateViewHolder()
yonBindViewHolder()
. - El método
onCreateViewHolder()
muestra unViewHolder
nuevo cuando RecyclerView lo necesita. - Dentro del método
onCreateViewHolder()
, crea un nuevo objetoView
y auméntalo desde el archivo de diseñoitem_list_item.xml
usando la clase de vinculación generada automáticamente,ItemListItemBinding
. - Implementa el método
onBindViewHolder()
. Obtén el elemento actual con el métodogetItem()
, pasando la posición. - Configura el objeto de escucha de clics en el objeto
itemView
y llama a la funciónonItemClicked()
dentro del objeto de escucha. - Define la clase
ItemViewHolder
y extiende la función desdeRecyclerView.ViewHolder.
Anula la funciónbind()
y pasa el objetoItem
. - Define un objeto complementario. Dentro del objeto complementario, define un
val
del tipoDiffUtil.ItemCallback<Item>()
llamadoDiffCallback
. Anula los métodos obligatoriosareItemsTheSame()
yareContentsTheSame()
, 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.
- En
ItemListAdapter.kt
, implementa la funciónbind()
en la claseItemViewHolder
. Vincula el objeto TextViewitemName
aitem.itemName
. Obtén el precio en formato de moneda con la función de extensióngetFormattedPrice()
y vincúlalo a la TextViewitemPrice
. Convierte el valor dequantityInStock
enString
y vincúlalo a la TextViewitemQuantity
. 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.
- Al comienzo de la clase
InventoryViewModel
, crea unval
llamadoallItems
del tipoLiveData<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.
- Llama a
getItems()
enitemDao
y asígnalo aallItems
. La funcióngetItems()
muestra unFlow
. Para consumir los datos como un valorLiveData
, usa la funciónasLiveData()
. 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.
- En
ItemListFragment
, al comienzo de la clase, declara una propiedadprivate
inmutable llamadaviewModel
del tipoInventoryViewModel
. Usa el delegadoby
para entregar la inicialización de propiedad a la claseactivityViewModels
. Pasa el constructorInventoryViewModelFactory
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Importa androidx.fragment.app.activityViewModels
cuando Android Studio lo solicite.
- Aún dentro de
ItemListFragment
, desplázate hasta la funciónonViewCreated()
. Debajo de la llamada asuper.onViewCreated()
, declara unval
llamadoadapter
. Inicializa la nueva propiedadadapter
con el constructor predeterminado;ItemListAdapter{}
no pasa nada. - Vincula el
adapter
recién creado arecyclerView
de la siguiente manera:
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
- Dentro de
onViewCreated()
, después de configurar el adaptador, adjunta un observador enallItems
para escuchar los cambios en los datos. - Dentro del observador, llama a
submitList()
enadapter
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)
}
}
- 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)
}
}
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.
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
- En
ItemListFragment
, desplázate hasta la funciónonViewCreated()
para actualizar la definición del adaptador. - Agrega una lambda como parámetro de constructor a
ItemListAdapter{}
.
val adapter = ItemListAdapter {
}
- Dentro de la expresión lambda, crea un objeto
val
llamadoaction
. Pronto corregirás el otro error.
val adapter = ItemListAdapter {
val action
}
- Llama al método
actionItemListFragmentToItemDetailFragment()
en el objetoItemListFragmentDirections
que pasa el elementoid
. Asigna aaction
el objetoNavDirections
que se muestra.
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
- Debajo de la definición
action
, recupera una instanciaNavController
usandothis.
findNavController
()
y llama anavigate()
cuando se pase laaction
. La definición del adaptador debería verse así:
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
- 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.
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.
- En
InventoryViewModel
, agrega una función llamadaretrieveItem()
que tome unaInt
para el ID del elemento y muestre unLiveData<Item>
. Pronto corregirás el error de la expresión que se muestra.
fun retrieveItem(id: Int): LiveData<Item> {
}
- Dentro de la nueva función, llama a
getItem()
enitemDao
y pasa el parámetroid
. La funcióngetItem()
muestra unFlow
. Para consumir el valor deFlow
comoLiveData
, llama a la funciónasLiveData()
y utiliza esto como lo que muestra la funciónretrieveItem()
. 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.
- En
ItemDetailFragment
, agrega una propiedad mutable llamadaitem
de la entidadItem
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 prefijolateinit
.
lateinit var item: Item
Importa com.example.inventory.data.Item
cuando Android Studio lo solicite.
- Al comienzo de la clase
ItemDetailFragment
, declara una propiedad inmutableprivate
llamadaviewModel
del tipoInventoryViewModel
. Usa el delegadoby
para entregar la inicialización de propiedad a la claseactivityViewModels
. Pasa el constructorInventoryViewModelFactory
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Importa androidx.fragment.app.activityViewModels
si Android Studio lo solicita.
- Aún en
ItemDetailFragment
, crea una funciónprivate
llamadabind()
que tome una instancia de la entidadItem
como parámetro y no muestre nada.
private fun bind(item: Item) {
}
- Implementa la función
bind()
, que es similar a lo que hiciste enItemListAdapter
. Establece la propiedadtext
de la TextViewitemName
enitem.itemName
. Llama agetFormattedPrice
()
en la propiedaditem
para dar formato al valor de precio y establecerlo en la propiedadtext
de la TextViewitemPrice
. Convierte el valorquantityInStock
enString
y configúralo en la propiedadtext
de la TextViewitemQuantity
.
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- Actualiza la función
bind()
a fin de usar la función de alcanceapply{}
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()
}
}
- Aún en
ItemDetailFragment
, anulaonViewCreated()
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- En uno de los pasos anteriores, pasaste el ID de elemento como un argumento de navegación a
ItemDetailFragment
desdeItemListFragment
. Dentro deonViewCreated()
, debajo de la llamada a la función de superclase, crea una variable inmutable llamadaid
. Recupera y asigna el argumento de navegación a esta nueva variable.
val id = navigationArgs.itemId
- Ahora, usarás esta variable
id
para recuperar los detalles del elemento. Aún dentro deonViewCreated()
, llama a la funciónretrieveItem()
en elviewModel
pasando elid
. Adjunta un observador al valor que se muestra pasando elviewLifecycleOwner
y una lambda.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
}
- Dentro de la expresión lambda, pasa
selectedItem
como el parámetro que contiene la entidadItem
recuperada de la base de datos. En el cuerpo de la función lambda, asigna el valorselectedItem
aitem
. Llama a la funciónbind()
pasando elitem
. 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)
}
}
- 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.
- 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:
- En
InventoryViewModel
, agrega una función privada llamadaupdateItem()
que tome una instancia de la clase de entidadItem
y no muestre nada.
private fun updateItem(item: Item) {
}
- Implementa el nuevo método,
updateItem()
. Para llamar al método de suspensiónupdate()
desde la claseItemDao
, inicia una corrutina con elviewModelScope
. Dentro del bloque de inicio, realiza una llamada a la funciónupdate()
enitemDao
pasando elitem
. El método completado debe verse de la siguiente manera.
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
- Dentro de
InventoryViewModel
, agrega otro método llamadosellItem()
que tome una instancia de la clase de entidadItem
y no muestre nada.
fun sellItem(item: Item) {
}
- Dentro de la función
sellItem()
, agrega una condiciónif
para verificar siitem.quantityInStock
es mayor que0
.
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)
- Vuelve a la función
sellItem()
enInventoryViewModel
. Dentro del bloqueif
, crea una nueva propiedad inmutable llamadanewItem
. Llama a la funcióncopy()
en la instanciaitem
y pasa elquantityInStock
actualizado, lo que disminuye la acción en1
.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
- Debajo de la definición de
newItem
, haz una llamada a la funciónupdateItem()
y pasa la nueva entidad actualizada, que esnewItem
. 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)
}
}
- Para agregar la función de stock para vender, ve a
ItemDetailFragment
. Desplázate hasta el final de la funciónbind()
. Dentro del bloqueapply
, establece un objeto de escucha de clics en el botón Sell y llama a la funciónsellItem()
enviewModel
.
private fun bind(item: Item) {
binding.apply {
...
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 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.
- 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.
- 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 que0
. Asigna el nombreisStockAvailable()
a la función, que toma una instancia deItem
y muestra unBoolean
.
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
- Ve a
ItemDetailFragment
y desplázate hasta la funciónbind()
. Dentro del bloque apply, llama a la funciónisStockAvailable()
enviewModel
pasando elitem
. Establece el valor que se muestra en la propiedadisEnabled
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) }
}
}
- 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.
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:
- En
InventoryViewModel
, agrega una nueva función llamadadeleteItem()
, que toma una instancia de la clase de entidadItem
llamadaitem
y no muestra nada. Dentro de la funcióndeleteItem()
, inicia una corrutina conviewModelScope
. Dentro del bloquelaunch
, llama al métododelete()
enitemDao
pasando elitem
.
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
- En
ItemDetailFragment
, desplázate hasta el inicio de la funcióndeleteItem()
. Llama adeleteItem()
en elviewModel
y pasaitem
. La instanciaitem
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()
}
- Aún dentro de
ItemDetailFragment
, desplázate hasta la funciónshowConfirmationDialog()
. 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óndeleteItem()
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:
- En
ItemDetailFragment
, al final de la funciónbind()
, dentro del bloqueapply
, configura el objeto de escucha de clics en el botón para borrar. Llama ashowConfirmationDialog()
dentro de la lambda del objeto de escucha click.
private fun bind(item: Item) {
binding.apply {
...
deleteItem.setOnClickListener { showConfirmationDialog() }
}
}
- 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.
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
- En
ItemDetailFragment
, agrega una nueva funciónprivate
llamadaeditItem()
que no tome parámetros ni muestre nada. En el siguiente paso, volverás a usarfragment_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.
- Dentro de la función
editItem()
, crea una variable inmutable llamadaaction
. Realiza una llamada aactionItemDetailFragmentToAddItemFragment()
enItemDetailFragmentDirections
pasando la string del título,edit_fragment_title
y el elementoid
. Asigna el valor mostrado aaction
. Debajo de la definición deaction
, llama athis.findNavController().navigate()
y pasaaction
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)
}
- Aún en
ItemDetailFragment
, desplázate hasta la funciónbind()
. Dentro del bloqueapply
, establece el objeto de escucha de clics en el BAF y llama a la funcióneditItem()
desde la lambda para navegar a la pantalla Edit Item.
private fun bind(item: Item) {
binding.apply {
...
editItem.setOnClickListener { editItem() }
}
}
- 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.
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
.
- En
AddItemFragment
, agrega una nueva funciónprivate
para vincular los campos de texto con los detalles de la entidad. Asigna el nombrebind()
a la función que toma instancias de la clase de entidad Item y no muestra ningún resultado.
private fun bind(item: Item) {
}
- La implementación de la función
bind()
es muy similar a la que realizaste antes enItemDetailFragment
. Dentro de la funciónbind()
, redondea el precio a dos lugares decimales con la funciónformat()
y asígnalo a unval
llamadoprice
, como se muestra a continuación.
val price = "%.2f".format(item.itemPrice)
- Debajo de la definición
price
, usa la función de permisoapply
en la propiedadbinding
, como se muestra a continuación.
binding.apply {
}
- Dentro del bloque de código de la función de alcance
apply
, estableceitem.itemName
en la propiedad de texto deitemName
. Usa la funciónsetText()
y pasa la stringitem.itemName
yTextView.BufferType.SPANNABLE
comoBufferType
.
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
Importa android.widget.TextView
si Android Studio lo solicita.
- Al igual que en el paso anterior, establece la propiedad de texto del precio
EditText
, como se muestra a continuación. Para establecer la propiedadtext
de la cantidad de EditText, recuerda convertir elitem.quantityInStock
enString
. 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)
}
}
- Dentro del
AddItemFragment
, desplázate hasta la funciónonViewCreated()
. Después de la llamada a la función superclase, crea unval
llamadoid
y recuperaitemId
de los argumentos de navegación.
val id = navigationArgs.itemId
- Agrega un bloque
if-else
con una condición para verificar siid
es mayor que cero y mueve al bloqueelse
el objeto de escucha del botón Save. Dentro del bloqueif
, se recupera la entidad conid
y se agrega un observador. Dentro del observador, actualiza la propiedaditem
y llama abind()
pasando elitem
. 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()
}
}
}
- 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.
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!
- En
InventoryViewModel
, agrega una funciónprivate
llamadagetUpdatedItemEntry()
que se tome enInt
y tres strings para los detalles de la entidad denominadasitemName
,itemPrice
yitemCount
. Muestra una instancia deItem
de la función. El código se proporciona como referencia.
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
}
- 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 instanciaItem
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()
)
}
- Dentro del
InventoryViewModel
, agrega otra función llamadaupdateItem()
. Esta función también toma unInt
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
) {
}
- Dentro de la función
updateItem()
, realiza una llamada a la funcióngetUpdatedItemEntry()
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 llamadaupdatedItem
.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
- Justo debajo de la llamada a la función
getUpdatedItemEntry()
, realiza una llamada a la funciónupdateItem()
pasando elupdatedItem
. 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)
}
- Regresa a
AddItemFragment
y agrega una función privada llamadaupdateItem()
sin parámetros y que no muestre nada. Dentro de la función, agrega una condiciónif
para validar la entrada del usuario llamando a la funciónisEntryValid()
.
private fun updateItem() {
if (isEntryValid()) {
}
}
- Dentro del bloque
if
, realiza una llamada aviewModel.updateItem()
pasando los detalles de la entidad. Usa elitemId
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()
)
- Debajo de la llamada a función
updateItem()
, define un elementoval
llamadoaction
. Llama aactionAddItemFragmentToItemListFragment()
enAddItemFragmentDirections
y asigna el valor que se muestra aaction
. Navega aItemListFragment
, llama alfindNavController().navigate()
y pasaaction
.
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)
}
}
- Aún dentro de
AddItemFragment
, desplázate hasta la funciónbind()
. Dentro del bloque de la función de alcancebinding.
apply
, establece el objeto de escucha de clics para el botón Save. Realiza una llamada a la funciónupdateItem()
dentro de la lambda, como se muestra a continuación.
private fun bind(item: Item) {
...
binding.apply {
...
saveAction.setOnClickListener { updateItem() }
}
}
- Ejecuta la app. Intenta editar elementos de inventario. Deberías poder editar cualquier elemento de la base de datos de la app de Inventory.
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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se 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 deLiveData
, usa la funciónasLiveData()
. - 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
- Cómo pasar datos entre destinos
- String de Android
- Android Formatter
- Cómo depurar tu base de datos con el Inspector de bases de datos
- Cómo guardar contenido en una base de datos local con Room
Referencias de la API
Referencias de Kotlin