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 mediante Room. Usarás una LazyColumn
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
- Saber crear la base de datos SQLite e interactuar con ella mediante la biblioteca Room
- Saber crear una entidad, un DAO y clases de bases de datos
- Saber usar un objeto de acceso a datos (DAO) para asignar funciones de Kotlin a consultas en SQL
- Saber mostrar elementos de lista en una
LazyColumn
. - 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
- Una app de Inventory que muestra una lista de elementos de inventario y puede actualizar, editar y borrar elementos de la base de datos de la app mediante Room
Requisitos
- Una computadora con Android Studio
2. Descripción general de la app de partida
En este codelab, se usa el código de solución de la app de Inventory del codelab anterior, Cómo conservar datos con Room, como código de partida. La app de partida ya guarda datos con la biblioteca de persistencias Room. El usuario puede usar la pantalla Add Item para agregar datos a la base de datos de la app.
En este codelab, extenderás la app para leer y mostrar los datos, y actualizar y borrar entidades en la base de datos usando la biblioteca Room.
Descarga el código de partida para este codelab
Para comenzar, descarga el código de partida:
$ 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 room
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de partida para este codelab, míralo en GitHub.
3. Actualiza el estado de la IU
En esta tarea, agregarás una LazyColumn
a la app para mostrar los datos almacenados en la base de datos.
Explicación de la función de componibilidad de HomeScreen
- Abre el archivo
ui/home/HomeScreen.kt
y observa el elemento componibleHomeScreen()
.
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
// Top app with app title
},
floatingActionButton = {
FloatingActionButton(
// onClick details
) {
Icon(
// Icon details
)
}
},
) { innerPadding ->
// Display List header and List of Items
HomeBody(
itemList = listOf(), // Empty list is being passed in for itemList
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
.fillMaxSize()
)
}
Esta función de componibilidad muestra los siguientes elementos:
- La barra superior de la aplicación con el título
- El botón de acción flotante (BAF) para agregar nuevos elementos al inventario
- La función de componibilidad
HomeBody()
La función de componibilidad HomeBody()
muestra elementos de inventario en función de la lista que se pasó. Como parte de la implementación del código de partida, se pasa una lista vacía (listOf()
) a la función de componibilidad HomeBody()
. Para pasar la lista de inventario a este elemento componible, debes recuperar los datos del inventario del repositorio y pasarlos a HomeViewModel
.
Emite un estado de IU en HomeViewModel
Cuando agregaste métodos a ItemDao
para obtener elementos (getItem()
y getAllItems()
), especificaste un Flow
como el tipo de datos que se muestra. Recuerda que un Flow
representa una transmisión de datos genérica. Si se devuelve un Flow
, solo debes llamar explícitamente a los métodos desde el DAO una vez para un ciclo de vida determinado. Room controla las actualizaciones de los datos subyacentes de manera asíncrona.
Obtener datos de un flujo se denomina recopilación de un flujo. Cuando recopiles datos desde un flujo en la capa de la IU, debes tener en cuenta algunos aspectos.
- Los eventos de ciclo de vida, como los cambios de configuración, por ejemplo, la rotación del dispositivo, hacen que se vuelva a crear la actividad. Esto provoca la recomposición y la recopilación de tu
Flow
nuevamente. - Querrás que los valores se almacenen en caché como estados para que los datos existentes no se pierdan entre los eventos de ciclo de vida.
- Los flujos deben cancelarse si no quedan observadores, como después de que finalice el ciclo de vida de un elemento componible.
La forma recomendada de exponer un Flow
desde un ViewModel
es con un StateFlow
. El uso de un StateFlow
permite guardar y observar los datos, sin importar el ciclo de vida de la IU. Para convertir un Flow
en un StateFlow
, usa el operador stateIn
.
El operador stateIn
tiene tres parámetros que se explican a continuación:
scope
: ElviewModelScope
define el ciclo de vida delStateFlow
. Cuando se cancela elviewModelScope
, también se cancela elStateFlow
.started
: La canalización solo debe estar activa cuando la IU sea visible. Se usaSharingStarted.WhileSubscribed()
para lograr esto. Para configurar una demora (en milisegundos) entre la desaparición del último suscriptor y la detención de la corrutina de uso compartido, pasaTIMEOUT_MILLIS
al métodoSharingStarted.WhileSubscribed()
.initialValue
: Establece el valor inicial del flujo de estado enHomeUiState()
.
Una vez que conviertas tu Flow
en StateFlow
, podrás recopilarlo con el método collectAsState()
y convertir sus datos en State
del mismo tipo.
En este paso, recuperarás todos los elementos de la base de datos de Room como una API observable de StateFlow
para el estado de la IU. Cuando cambian los datos de Inventory de Room, la IU se actualiza automáticamente.
- Abre el archivo
ui/home/HomeViewModel.kt
, que contiene una constanteTIMEOUT_MILLIS
y una clase de datosHomeUiState
con una lista de elementos como parámetro de constructor.
// No need to copy over, this code is part of starter code
class HomeViewModel : ViewModel() {
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class HomeUiState(val itemList: List<Item> = listOf())
- Dentro de la clase
HomeViewModel
, declara unval
llamadohomeUiState
del tipoStateFlow<HomeUiState>
. En breve, resolverás el error de inicialización.
val homeUiState: StateFlow<HomeUiState>
- Llama a
getAllItemsStream()
enitemsRepository
y asígnalo alhomeUiState
que acabas de declarar.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
Ahora se muestra el error que indica que la referencia itemsRepository no está resuelta. Para solucionar este error, debes pasar el objeto ItemsRepository
a HomeViewModel
.
- Agrega un parámetro de constructor del tipo
ItemsRepository
a la claseHomeViewModel
.
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
- En el archivo
ui/AppViewModelProvider.kt
, en el inicializadorHomeViewModel
, pasa el objetoItemsRepository
como se muestra.
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
- Regresa al archivo
HomeViewModel.kt
. Observa el error de discrepancia de tipos. Para resolver esto, agrega un mapa de transformación como se muestra a continuación.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
Android Studio aún te muestra un error de discrepancia de tipos. Este error se debe a que homeUiState
es del tipo StateFlow
y getAllItemsStream()
devuelve un Flow
.
- Usa el operador
stateIn
para convertirFlow
enStateFlow
.StateFlow
es la API observable para el estado de la IU, que permite que la IU se actualice sola.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
- Compila la app para asegurarte de que no haya errores en el código. No habrá cambios visuales.
4. Muestra los datos de inventario
En esta tarea, recopilarás y actualizarás el estado de la IU en HomeScreen
.
- En el archivo
HomeScreen.kt
, en la función de componibilidadHomeScreen
, agrega un parámetro de función nuevo del tipoHomeViewModel
y, luego, inicialízalo.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- En la función de componibilidad
HomeScreen
, agrega unval
llamadohomeUiState
para recopilar el estado de la IU deHomeViewModel
. UsacollectAsState
()
, que recopila valores de esteStateFlow
y representa su valor más reciente medianteState
.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
- Actualiza la llamada a la función
HomeBody()
y pasahomeUiState.itemList
al parámetroitemList
.
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- Ejecuta la app. Observa que la lista de inventario muestra si guardaste elementos en la base de datos de tu app. Si la lista está vacía, agrega algunos elementos de inventario a la base de datos de la app.
5. Prueba tu base de datos
En los codelabs anteriores, se analiza la importancia de probar el código. En esta tarea, agregarás algunas pruebas de unidades para probar tus consultas DAO y, luego, agregarás más pruebas a medida que avances en el codelab.
El enfoque recomendado para probar la implementación de la base de datos es escribir una prueba JUnit que se ejecute en un dispositivo Android. Como estas pruebas no requieren la creación de una actividad, se ejecutan más rápido que tus pruebas de IU.
- En el archivo
build.gradle.kts (Module :app)
, observa las siguientes dependencias para Espresso y JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- Cambia a la vista Project y haz clic con el botón derecho en src > New > Directory para crear un conjunto de orígenes de prueba para tu pruebas.
- Selecciona androidTest/kotlin en la ventana emergente New Directory.
- Crea una clase de Kotlin llamada
ItemDaoTest.kt
. - Anota la clase
ItemDaoTest
con@RunWith(AndroidJUnit4::class)
. Ahora tu clase se verá como el siguiente código de ejemplo:
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- Dentro de la clase, agrega variables privadas
var
de tipoItemDao
yInventoryDatabase
.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- Agrega una función para crear la base de datos y anótala con
@Before
para que pueda ejecutarse antes de cada prueba. - Dentro del método, inicializa
itemDao
.
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
// Using an in-memory database because the information stored here disappears when the
// process is killed.
inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
// Allowing main thread queries, just for testing.
.allowMainThreadQueries()
.build()
itemDao = inventoryDatabase.itemDao()
}
En esta función, se usa una base de datos en la memoria y no se conserva en el disco. Para hacerlo, usa la función inMemoryDatabaseBuilder(). Debes hacerlo porque la información no debe ser persistente, sino que debe borrarse cuando se cierra el proceso. Estás ejecutando las consultas DAO en el subproceso principal con .allowMainThreadQueries()
solo para pruebas.
- Agrega otra función para cerrar la base de datos. Anótala con
@After
para cerrar la base de datos y ejecutarla después de cada prueba.
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- Declara elementos en la clase
ItemDaoTest
para que los use la base de datos, como se muestra en el siguiente código de ejemplo:
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- Agrega funciones de utilidad para agregar un elemento y, luego, dos elementos a la base de datos. Más adelante, usarás estas funciones en la prueba. Márcalas como
suspend
para que puedan ejecutarse en una corrutina.
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- Escribe una prueba para insertar un solo elemento en la base de datos,
insert()
. Asigna el nombredaoInsert_insertsItemIntoDB
a la prueba y anótala con@Test
.
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
addOneItemToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
}
En esta prueba, usarás la función de utilidad addOneItemToDb()
para agregar un elemento a la base de datos. Luego, debes leer el primer elemento de la base de datos. Con assertEquals()
, puedes comparar el valor esperado con el real. Ejecuta la prueba en una corrutina nueva con runBlocking{}
. Esta configuración es la razón por la que marcas las funciones de utilidad como suspend
.
- Ejecuta la prueba y asegúrate de que sea exitosa.
- Escribe otra prueba para
getAllItems()
desde la base de datos. Asigna el nombredaoGetAllItems_returnsAllItemsFromDB
a la prueba.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
En la prueba anterior, agregaste dos elementos a la base de datos dentro de una corrutina. Luego, leíste los dos elementos y los comparaste con los valores esperados.
6. 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 el estado de la IU del elemento, 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 elemento componible ItemDetailsScreen
. La función de componibilidad ItemDetailsScreen
ya está escrita y contiene tres elementos componibles de texto que muestran los detalles del elemento.
ui/item/ItemDetailsScreen.kt
Esta pantalla forma parte del código de partida y muestra los detalles de los elementos, que verás en un codelab posterior. En este codelab, no trabajarás en esa pantalla. ItemDetailsViewModel.kt
es el ViewModel
correspondiente de esta pantalla.
- En la función de componibilidad
HomeScreen
, observa la llamada a la funciónHomeBody()
.navigateToItemUpdate
se pasa al parámetroonItemClick
, al que se llama cuando haces clic en cualquier elemento de la lista.
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
- Abre
ui/navigation/InventoryNavGraph.kt
y observa el parámetronavigateToItemUpdate
en el elemento componibleHomeScreen
. Este parámetro especifica el destino de la navegación como la pantalla de detalles del elemento.
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
Ya se implementó esta parte de la funcionalidad de onItemClick
. Cuando haces clic en el elemento de la lista, la app navega a la pantalla de detalles del elemento.
- Haz clic en cualquier elemento de la lista del inventario para ver la pantalla de detalles del elemento con campos vacíos.
Para completar los campos de texto con los detalles del elemento, debes recopilar el estado de la IU en ItemDetailsScreen()
.
- En
UI/Item/ItemDetailsScreen.kt
, agrega un parámetro nuevo al elemento componibleItemDetailsScreen
del tipoItemDetailsViewModel
y usa el método de fábrica para inicializarlo.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun ItemDetailsScreen(
navigateToEditItem: (Int) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- Dentro del elemento componible
ItemDetailsScreen()
, crea unval
llamadouiState
para recopilar el estado de la IU. UsacollectAsState()
para recopilaruiState
StateFlow
y representar su valor más reciente medianteState
. Android Studio muestra un error de referencia sin resolver.
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- Para resolver el error, crea un
val
llamadouiState
del tipoStateFlow<ItemDetailsUiState>
en la claseItemDetailsViewModel
. - Recupera los datos del repositorio de elementos y asígnalos a
ItemDetailsUiState
con la función de extensióntoItemDetails()
. La función de extensiónItem.toItemDetails()
ya está escrita como parte del código de partida.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(itemDetails = it.toItemDetails())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ItemDetailsUiState()
)
- Pasa
ItemsRepository
aItemDetailsViewModel
para resolver el errorUnresolved reference: itemsRepository
.
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- En
ui/AppViewModelProvider.kt
, actualiza el inicializador paraItemDetailsViewModel
como se muestra en el siguiente fragmento de código:
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Vuelve a
ItemDetailsScreen.kt
y observa que se resolvió el error en el elemento componibleItemDetailsScreen()
. - En el elemento componible
ItemDetailsScreen()
, actualiza la llamada a funciónItemDetailsBody()
y pasauiState.value
al argumentoitemUiState
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Observa las implementaciones de
ItemDetailsBody()
yItemInputForm()
. Estás pasando el elementoitem
seleccionado actual deItemDetailsBody()
aItemDetails()
.
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
//...
) {
var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
ItemDetails(
item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
)
//...
}
- Ejecuta la app. Cuando hagas clic en cualquier elemento de la lista en la pantalla Inventory, se mostrará la pantalla Item Details.
- Observa que la pantalla ya no está en blanco. Muestra los detalles de la entidad recuperados de la base de datos de inventario.
- Presiona el botón Sell. No pasa nada.
En la siguiente sección, implementarás la funcionalidad del botón Sell.
7. Implementa la pantalla Item Details
ui/item/ItemEditScreen.kt
La pantalla Edit Item ya se te proporcionó como parte del código de partida.
Este diseño contiene elementos componibles de campo de texto para editar los detalles de cualquier elemento de inventario nuevo.
El código de esta app todavía no funciona por completo. Por ejemplo, en la pantalla Item Details, cuando presionas el botón Sell, la Quantity in Stock no disminuye. Cuando presionas el botón Delete, la app te muestra un cuadro de diálogo de confirmación. Sin embargo, si seleccionas el botón Yes, la app no borrará el elemento.
Por último, el botón BAF abre una pantalla Edit Item vacía.
En esta sección, implementarás las funcionalidades de los botones Sell, Delete y BAF.
8. Implementa la venta de artículos
En esta sección, extenderás las funciones de la app para implementar la funcionalidad de venta. Esta actualización incluye las siguientes tareas:
- Agregar una prueba para que la función DAO actualice una entidad
- Agregar una función a
ItemDetailsViewModel
para reducir la cantidad y actualizar la entidad en la base de datos de la app - Inhabilitar el botón Sell si la cantidad es cero
- En
ItemDaoTest.kt
, agrega una función llamadadaoUpdateItems_updatesItemsInDB()
sin parámetros. Anota con@Test
y@Throws(Exception::class)
.
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- Define la función y crea un bloque
runBlocking
. Llama aaddTwoItemsToDb()
dentro de ella.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- Actualiza las dos entidades con diferentes valores y llama a
itemDao.update
.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
- Recupera las entidades con
itemDao.getAllItems()
. Compáralas con la entidad actualizada y afirma.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- Asegúrate de que la función completada se vea de la siguiente manera:
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
- Ejecuta la prueba y asegúrate de que sea exitosa.
Agrega una función en ViewModel
.
- En
ItemDetailsViewModel.kt
, dentro de la claseItemDetailsViewModel
, agrega una función llamadareduceQuantityByOne()
sin parámetros.
fun reduceQuantityByOne() {
}
- Dentro de la función, inicia una corrutina con
viewModelScope.launch{}
.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
- Dentro del bloque
launch
, crea unval
llamadocurrentItem
y establécelo enuiState.value.toItem()
.
val currentItem = uiState.value.toItem()
El uiState.value
es del tipo ItemUiState
. Lo conviertes al tipo de entidad Item
con la función de extensión toItem
()
.
- Agrega una sentencia
if
para verificar siquality
es mayor que0
. - Llama a
updateItem()
enitemsRepository
y pasa elcurrentItem
actualizado. Usacopy()
para actualizar el valorquantity
y que la función se vea de la siguiente manera:
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
- Vuelve a
ItemDetailsScreen.kt
. - En el elemento componible
ItemDetailsScreen
, ve a la llamada a la funciónItemDetailsBody()
. - En la lambda
onSellItem
, llama aviewModel.reduceQuantityByOne()
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Ejecuta la app.
- En la pantalla Inventory, haz clic en un elemento de la lista. Cuando aparezca la pantalla Item Details, presiona Sell y observa que el valor de la cantidad disminuye en uno.
- En la pantalla Item Details, presiona continuamente el botón Sell hasta que la cantidad sea cero.
Cuando la cantidad llegue a cero, vuelve a presionar Sell. No hay cambios visuales porque la función reduceQuantityByOne()
comprueba si la cantidad es mayor que cero antes de actualizarla.
Para proporcionar mejores comentarios a los usuarios, puedes inhabilitar el botón Sell cuando no haya ningún artículo para vender.
- En la clase
ItemDetailsViewModel
, establece el valoroutOfStock
segúnit
.quantity
en la transformación demap
.
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- Ejecuta la app. Observa que la app inhabilita el botón Sell cuando la cantidad en stock es cero.
Felicitaciones por implementar la función Sell de elementos en tu app.
Entidad Delete item
Al igual que con la tarea anterior, debes extender las funciones de tu app con la implementación de la función de borrar. Esta función es mucho más fácil de implementar que la función de venta. El proceso incluye las siguientes tareas:
- Agregar una prueba para la consulta de eliminación de DAO
- Agregar una función en la clase
ItemDetailsViewModel
para borrar una entidad de la base de datos - Actualizar el elemento
ItemDetailsBody
componible
Agrega una prueba de DAO
- En
ItemDaoTest.kt
, agrega una prueba llamadadaoDeleteItems_deletesAllItemsFromDB()
.
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
- Inicia una corrutina con
runBlocking {}
.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- Agrega dos elementos a la base de datos y llama a
itemDao.delete()
en esos dos elementos para borrarlos de la base de datos.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- Recupera las entidades de la base de datos y verifica que la lista esté vacía. La prueba completada debería verse de la siguiente manera:
import org.junit.Assert.assertTrue
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
val allItems = itemDao.getAllItems().first()
assertTrue(allItems.isEmpty())
}
Agrega la función de borrar en ItemDetailsViewModel
- En
ItemDetailsViewModel
, agrega una nueva función llamadadeleteItem()
que no tome parámetros ni devuelva nada. - Dentro de la función
deleteItem()
, agrega una llamada a la funciónitemsRepository.deleteItem()
y pasauiState.value.
toItem
()
.
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
En esta función, conviertes uiState
del tipo itemDetails
al tipo de entidad Item
con la función de extensión toItem
()
.
- En el elemento componible
ui/item/ItemDetailsScreen
, agrega un elementoval
llamadocoroutineScope
y establécelo enrememberCoroutineScope()
. Este enfoque muestra un alcance de corrutinas vinculado a la composición a la que se llama (elemento componibleItemDetailsScreen
).
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Desplázate hasta la función
ItemDetailsBody()
. - Inicia una corrutina con
coroutineScope
dentro de la lambdaonDelete
. - Dentro del bloque
launch
, llama al métododeleteItem()
enviewModel
.
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- Después de borrar el elemento, vuelve a la pantalla del inventario.
- Llama a
navigateBack()
después de la llamada a funcióndeleteItem()
.
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- Dentro del archivo
ItemDetailsScreen.kt
, desplázate hasta la funciónItemDetailsBody()
.
Esta función es parte del código de partida. Este elemento componible muestra un diálogo de alerta para obtener la confirmación del usuario antes de borrar el elemento y llama a la función deleteItem()
cuando presionas Yes.
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
/*...*/
) {
//...
if (deleteConfirmationRequired) {
DeleteConfirmationDialog(
onDeleteConfirm = {
deleteConfirmationRequired = false
onDelete()
},
//...
)
}
}
}
Cuando presionas No, la app cierra el diálogo de alerta. La función showConfirmationDialog()
muestra la siguiente alerta:
- Ejecuta la app.
- Selecciona un elemento de lista en la pantalla Inventory.
- En la pantalla Item Details, presiona Delete.
- Presiona Yes en el diálogo de alerta y la app regresará a la pantalla Inventory.
- Confirma que la entidad que borraste ya no esté en la base de datos de la app.
Felicitaciones por implementar la función de borrar.
Edita la entidad del elemento
Al igual que en las secciones anteriores, en esta sección, agregarás otra mejora de funciones a la app que edita la entidad de un elemento.
A continuación, te mostramos los pasos rápidos para editar una entidad en la base de datos de la app:
- Agrega una prueba a la consulta de DAO de obtención de elementos de prueba.
- Propaga los campos de texto y la pantalla Edit Item con los detalles de la entidad.
- Actualiza la entidad en la base de datos con Room.
Agrega una prueba de DAO
- En
ItemDaoTest.kt
, agrega una prueba llamadadaoGetItem_returnsItemFromDB()
.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- Define la función. Dentro de la corrutina, agrega un elemento a la base de datos.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
- Recupera la entidad de la base de datos con la función
itemDao.getItem()
y establécela en unval
llamadoitem
.
val item = itemDao.getItem(1)
- Compara el valor real con el valor recuperado y afirma con
assertEquals()
. La prueba completada se verá de la siguiente manera:
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- Ejecuta la prueba y asegúrate de que sea exitosa.
Propaga campos de texto
Si ejecutas la app, ve a la pantalla Item Details y haz clic en el BAF. Verás que el título de la pantalla ahora es Edit Item. Sin embargo, todos los campos de texto están vacíos. En este paso, completarás los campos de texto en la pantalla Edit Item con los detalles de la entidad.
- En
ItemDetailsScreen.kt
, desplázate hasta el elemento componibleItemDetailsScreen
. - En
FloatingActionButton()
, cambia el argumentoonClick
para incluiruiState.value.itemDetails.id
, que es elid
de la entidad seleccionada. Debes usar esteid
para recuperar los detalles de la entidad.
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
- En la clase
ItemEditViewModel
, agrega un bloqueinit
.
init {
}
- Dentro del bloque
init
, inicia una corrutina conviewModelScope
.
launch
.
import kotlinx.coroutines.launch
viewModelScope.launch { }
- Dentro del bloque
launch
, recupera los detalles de la entidad conitemsRepository.getItemStream(itemId)
.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
init {
viewModelScope.launch {
itemUiState = itemsRepository.getItemStream(itemId)
.filterNotNull()
.first()
.toItemUiState(true)
}
}
En este bloque de lanzamiento, se agrega un filtro para devolver un flujo que solo contenga valores que no son nulos. Con toItemUiState()
, conviertes la entidad item
en ItemUiState
. Pasas el valor actionEnabled
como true
para habilitar el botón Save.
Para resolver el error Unresolved reference: itemsRepository
, debes pasar ItemsRepository
como una dependencia al modelo de vista.
- Agrega un parámetro de constructor a la clase
ItemEditViewModel
.
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
- En el archivo
AppViewModelProvider.kt
, en el inicializadorItemEditViewModel
, agrega el objetoItemsRepository
como argumento.
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Ejecuta la app.
- Ve a Item Details y presiona el BAF .
- Observa que los campos se propagan con los detalles del elemento.
- Edita la cantidad en stock o cualquier otro campo, y presiona el botón Save.
No pasa nada. Eso se debe a que no estás actualizando la entidad en la base de datos de la app. Solucionarás este problema en la próxima sección.
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 ItemEditScreen
.
Es hora de volver a programar.
- En la clase
ItemEditViewModel
, agrega una función llamadaupdateUiState()
que tome un objetoItemUiState
y no devuelva nada. Esta función actualizaitemUiState
con los valores nuevos que ingresa el usuario.
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
En esta función, asignarás el itemDetails
pasado a itemUiState
y actualizarás el valor isEntryValid
. La app habilita el botón Save si itemDetails
es true
. Establece este valor en true
solo si la entrada que ingresa el usuario es válida.
- Ve al archivo
ItemEditScreen.kt
. - En el elemento
ItemEditScreen
componible, desplázate hacia abajo hasta la llamada a funciónItemEntryBody()
. - Establece el valor del argumento
onItemValueChange
en la nueva funciónupdateUiState
.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- Ejecuta la app.
- Ve a la pantalla Edit Item.
- Deja uno de los valores de la entidad en blanco para que no sea válido. Observa cómo el botón Save se inhabilita automáticamente.
- Vuelve a la clase
ItemEditViewModel
y agrega una funciónsuspend
llamadaupdateItem()
que no tome nada. Usarás esta función para guardar la entidad actualizada en la base de datos de Room.
suspend fun updateItem() {
}
- Dentro de la función
getUpdatedItemEntry()
, agrega una condiciónif
para validar la entrada del usuario usando la funciónvalidateInput()
. - Realiza una llamada a la función
updateItem()
enitemsRepository
y pasaitemUiState.itemDetails.
toItem
()
. Las entidades que se pueden agregar a la base de datos de Room deben ser del tipoItem
. La función completada se ve de la siguiente manera:
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
- Vuelve al elemento
ItemEditScreen
componible. Necesitas un alcance de corrutinas para llamar a la funciónupdateItem()
. Crea un val llamadocoroutineScope
y establécelo enrememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- En la llamada a función
ItemEntryBody()
, actualiza el argumento de funciónonSaveClick
para iniciar una corrutina encoroutineScope
. - Dentro del bloque
launch
, llama aupdateItem()
enviewModel
y navega hacia atrás.
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
La llamada a la función ItemEntryBody()
completada se ve de la siguiente manera:
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- Ejecuta la app y edita los elementos del inventario. Ahora puedes editar cualquier elemento en la base de datos de la app de Inventory.
Felicitaciones por crear tu primera app que usa Room para administrar la base de datos.
9. 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:
10. Más información
Documentación para desarrolladores de Android
- 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
- Cómo probar y depurar tu base de datos | Android Developers
Referencias de Kotlin