1. Antes de começar
Nos codelabs anteriores, você aprendeu a usar a biblioteca de persistência do Room, que é uma camada de abstração sobre um banco de dados SQLite, para armazenar dados de apps. Neste, você vai adicionar mais recursos ao app Inventory e aprender a ler, mostrar, atualizar e excluir dados do banco de dados SQLite usando o Room. Você vai usar uma LazyColumn
para mostrar e atualizar automaticamente os dados do banco de dados quando eles mudarem.
Pré-requisitos
- Saber criar e interagir com o banco de dados SQLite usando a biblioteca do Room.
- Saber criar uma entidade, um DAO e as classes de banco de dados.
- Saber usar um objeto de acesso a dados (DAO) para mapear funções Kotlin para consultas SQL.
- Saber mostrar itens de lista em uma
LazyColumn
. - Já ter feito o codelab anterior desta unidade, Persistir dados com o Room.
O que você vai aprender
- Ler e exibir entidades de um banco de dados SQLite.
- Atualizar e excluir entidades de um banco de dados SQLite usando a biblioteca do Room.
O que você vai criar
- Um app Inventory que mostra uma lista de itens de inventário e pode atualizar, editar e excluir itens do banco de dados do app usando o Room.
O que é necessário
- Um computador com o Android Studio.
2. Visão geral do app inicial
Este codelab usa o código de solução do app Inventory do codelab anterior, Persistir dados com o Room, como o código inicial. O app inicial já salva dados com a biblioteca de persistência do Room. O usuário pode usar a tela Add Item (adicionar item) para adicionar dados ao banco de dados do app.
Neste codelab, você vai ampliar o app para ler e mostrar os dados, além de atualizar e excluir entidades do banco de dados usando uma biblioteca do Room.
Fazer o download do código inicial para este codelab
Para começar, faça o download do código inicial:
$ 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
Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.
Confira o código inicial deste codelab no GitHub (link em inglês).
3. Atualizar o estado da IU
Nesta tarefa, você vai adicionar uma LazyColumn
ao app para mostrar os dados armazenados no banco de dados.
Tutorial da função combinável HomeScreen
- Abra o arquivo
ui/home/HomeScreen.kt
e observe a função combinávelHomeScreen()
.
@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()
)
}
Essa função mostra os itens abaixo:
- A barra de apps de cima com o título do app
- O botão de ação flutuante (FAB) para a adição de novos itens ao inventário
- A função combinável
HomeBody()
A função combinável HomeBody()
mostra itens de inventário com base na lista transmitida. Como parte da implementação do código inicial, uma lista vazia (listOf()
) é transmitida para a função combinável HomeBody()
. Para transmitir a lista de inventário a essa função combinável, é necessário extrair os dados do inventário do repositório e transmiti-los para o HomeViewModel
.
Emitir o estado da IU no HomeViewModel
Ao adicionar métodos ao ItemDao
para acessar itens (getItem()
e getAllItems()
), você especificou um Flow
como tipo de retorno. Um Flow
representa um stream de dados genérico. Ao retornar um Flow
, você só precisa chamar explicitamente os métodos do DAO uma vez para um determinado ciclo de vida. O Room gerencia as atualizações dos dados de maneira assíncrona.
A coleta de dados de um fluxo é chamada de coleta de um fluxo. Ao coletar de um fluxo na camada de interface, há alguns pontos a serem considerados.
- Eventos de ciclo de vida, como mudanças de configuração, por exemplo, a rotação do dispositivo, fazem com que a atividade seja recriada. Isso causa a recomposição e a coleta do
Flow
novamente. - Você quer que os valores sejam armazenados em cache como estado para que os dados atuais não sejam perdidos entre os eventos de ciclo de vida.
- Os fluxos precisam ser cancelados quando não há observadores restantes, como após o fim do ciclo de vida de um elemento combinável.
A forma recomendada de expor um Flow
de um ViewModel
é com um StateFlow
. O uso de um StateFlow
permite que os dados sejam salvos e observados, independente do ciclo de vida da interface. Para converter um Flow
em um StateFlow
, use o operador stateIn
.
O operador stateIn
tem três parâmetros explicados abaixo:
scope
: oviewModelScope
define o ciclo de vida doStateFlow
. Quando oviewModelScope
é cancelado, oStateFlow
também é.started
: o pipeline só fica ativo quando a IU está visível. OSharingStarted.WhileSubscribed()
é usado para fazer isso. Para configurar um atraso (em milissegundos) entre o desaparecimento do último assinante e a interrupção da corrotina de compartilhamento, transmita oTIMEOUT_MILLIS
para o métodoSharingStarted.WhileSubscribed()
.initialValue
: define o valor inicial do fluxo de estado comoHomeUiState()
.
Depois de converter o Flow
em um StateFlow
, você pode coletá-lo usando o método collectAsState()
, convertendo os dados no State
do mesmo tipo.
Nesta etapa, você vai extrair todos os itens no banco de dados do Room como uma API observável StateFlow
para o estado da interface. Quando os dados de inventário do Room mudam, a IU é atualizada automaticamente.
- Abra o arquivo
ui/home/HomeViewModel.kt
, que contém uma constanteTIMEOUT_MILLIS
e uma classe de dadosHomeUiState
com uma lista de itens como um parâmetro construtor.
// 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())
- Na classe
HomeViewModel
, declare umval
com o nomehomeUiState
e o tipoStateFlow<HomeUiState>
. Você vai resolver o erro de inicialização em breve.
val homeUiState: StateFlow<HomeUiState>
- Chame
getAllItemsStream()
emitemsRepository
e o atribua aohomeUiState
que você acabou de declarar.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
Agora você vai encontrar um erro: "Unresolved reference: itemsRepository" (Referência não resolvida: itemsRepository). Para resolver o erro de referência, transmita o objeto ItemsRepository
para o HomeViewModel
.
- Adicione um parâmetro construtor do tipo
ItemsRepository
à classeHomeViewModel
.
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
- No arquivo
ui/AppViewModelProvider.kt
, no inicializadorHomeViewModel
, transmita o objetoItemsRepository
conforme mostrado.
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
- Volte para o arquivo
HomeViewModel.kt
. Observe o erro de incompatibilidade de tipo. Para resolver isso, adicione um mapa de transformação, conforme mostrado abaixo.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
O Android Studio ainda vai mostrar um erro de incompatibilidade de tipo. Esse erro ocorre porque o homeUiState
é do tipo StateFlow
e getAllItemsStream()
retorna um Flow
.
- Use o operador
stateIn
para converter oFlow
em umStateFlow
. OStateFlow
é a API observável do estado da interface, o que permite que ela seja atualizada.
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()
)
- Crie o app para garantir que não haja erros no código. Não vai haver mudanças visuais.
4. Mostrar os dados do inventário
Nesta tarefa, você vai coletar e atualizar o estado da IU na HomeScreen
.
- No arquivo
HomeScreen.kt
, na função combinávelHomeScreen
, adicione e inicialize um novo parâmetro de função do tipoHomeViewModel
.
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)
)
- Na função combinável
HomeScreen
, adicione umval
com o nomehomeUiState
para coletar o estado da interface doHomeViewModel
. Você vai usarcollectAsState
()
, que coleta valores desseStateFlow
(link em inglês) e representa o valor mais recente emState
.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
- Atualize a chamada de função
HomeBody()
e transmitahomeUiState.itemList
para o parâmetroitemList
.
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- Execute o app. A lista de inventário vai ser mostrada se você salvar itens no banco de dados do app. Se a lista estiver vazia, adicione alguns itens de inventário ao banco de dados do app.
5. Testar seu banco de dados
Os codelabs anteriores discutem a importância de testar o código. Nesta tarefa, você vai adicionar alguns testes de unidade para testar as consultas do DAO e, em seguida, vai adicionar mais testes à medida que avança pelo codelab.
A abordagem recomendada para testar a implementação do banco de dados é gravar um teste JUnit executado em um dispositivo Android. Como esses testes não exigem a criação de uma atividade, sua execução vai ser mais rápida que os testes de interface.
- No arquivo
build.gradle.kts (Module :app)
, observe as seguintes dependências do Espresso e do JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- Alterne para a visualização Project e clique com o botão direito do mouse em src > New > Directory para criar um conjunto de origem para testes.
- Selecione androidTest/kotlin no menu pop-up New Directory.
- Crie uma classe do Kotlin com o nome
ItemDaoTest.kt
. - Adicione a anotação
@RunWith(AndroidJUnit4::class)
à classeItemDaoTest
. Agora, a classe vai ser parecida com este código de exemplo:
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- Dentro da classe, adicione variáveis
var
particulares dos tiposItemDao
eInventoryDatabase
.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- Adicione uma função para criar o banco de dados e adicione a anotação
@Before
para que ela possa ser executada antes de cada teste. - No método, inicialize
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()
}
Nessa função, você usa um banco de dados na memória e não o armazena no disco. Para isso, use a função inMemoryDatabaseBuilder(). Isso é necessário porque as informações não precisam ser mantidas, mas precisam ser excluídas quando o processo é encerrado. Você está executando as consultas de DAO na linha de execução principal com .allowMainThreadQueries()
, apenas para teste.
- Adicione outra função para fechar o banco de dados. Adicione a anotação
@After
para fechar o banco de dados e ser executada depois de cada teste.
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- Declare os itens na classe
ItemDaoTest
para o uso do banco de dados, conforme mostrado no exemplo de código a seguir:
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- Adicione funções utilitárias para acrescentar um item e, em seguida, dois itens ao banco de dados. Depois, você vai usar essas funções no seu teste. Marque-as como
suspend
para que possam ser executadas em uma corrotina.
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- Crie um teste para inserir um único item no banco de dados,
insert()
. Dê o nomedaoInsert_insertsItemIntoDB
ao teste e adicione a anotação@Test
a ele.
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)
}
Nesse teste, você usa a função utilitária addOneItemToDb()
para adicionar um item ao banco de dados. Depois, você vai ler o primeiro item no banco de dados. Com assertEquals()
, você compara o valor esperado com o valor real. Execute o teste em uma nova corrotina com runBlocking{}
. Essa configuração é o motivo pelo qual você marcou as funções utilitárias como suspend
.
- Execute o teste e confira se ele é aprovado.
- Crie outro teste para
getAllItems()
no banco de dados. Dê o nomedaoGetAllItems_returnsAllItemsFromDB
ao teste.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
No teste acima, você adiciona dois itens ao banco de dados dentro de uma corrotina. Depois, leia os dois itens e compare-os com os valores esperados.
6. Mostrar detalhes do item
Nesta tarefa, você vai ler e mostrar as informações da entidade na tela Item Details. Use o estado da interface do item, como nome, preço e quantidade do banco de dados do app de inventário, e mostre-os na tela Item Details (detalhes do item) usando a função combinável ItemDetailsScreen
. A função combinável ItemDetailsScreen
já está pré-criada para você e contém três elementos combináveis de texto que mostram os detalhes do item.
ui/item/ItemDetailsScreen.kt
Essa tela faz parte do código inicial e mostra os detalhes dos itens, que serão mostrados em um próximo codelab. Você não vai trabalhar com essa tela neste codelab. O ItemDetailsViewModel.kt
é o modelo de visualização correspondente ao ViewModel
dessa tela.
- Na função combinável
HomeScreen
, observe a chamada de funçãoHomeBody()
. OnavigateToItemUpdate
está sendo transmitido para o parâmetroonItemClick
, que é chamado quando você clica em qualquer item da lista.
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
- Abra
ui/navigation/InventoryNavGraph.kt
e observe o parâmetronavigateToItemUpdate
na função combinávelHomeScreen
. Esse parâmetro especifica o destino da navegação como a tela de detalhes do item.
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
Essa parte da funcionalidade onItemClick
já foi implementada para você. Quando você clica no item da lista, o app navega para a tela de detalhes do item.
- Clique em qualquer item da lista de inventário para conferir a tela de detalhes com campos vazios.
Para preencher os campos de texto com detalhes do item, é necessário coletar o estado da interface em ItemDetailsScreen()
.
- No
UI/Item/ItemDetailsScreen.kt
, adicione um novo parâmetro do tipoItemDetailsViewModel
ao elemento combinávelItemDetailsScreen
e use o método de fábrica para inicializá-lo.
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 do elemento combinável
ItemDetailsScreen()
, crie umval
com o nomeuiState
para coletar o estado da interface. UsecollectAsState()
para coletaruiState
StateFlow
e representar o valor mais recente usandoState
. O Android Studio mostra um erro de referência não resolvido.
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- Para resolver o erro, crie um
val
chamadouiState
do tipoStateFlow<ItemDetailsUiState>
na classeItemDetailsViewModel
. - Extraia os dados do repositório de itens e mapeie-os para
ItemDetailsUiState
usando a função de extensãotoItemDetails()
. A função de extensãoItem.toItemDetails()
já foi criada para você como parte do código inicial.
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()
)
- Transmita
ItemsRepository
aoItemDetailsViewModel
para resolver o erroUnresolved reference: itemsRepository
.
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- No
ui/AppViewModelProvider.kt
, atualize o inicializador para oItemDetailsViewModel
, conforme mostrado neste snippet de código:
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Volte para o
ItemDetailsScreen.kt
e observe que o erro na função combinávelItemDetailsScreen()
foi resolvido. - Na função combinável
ItemDetailsScreen()
, atualize a chamada de funçãoItemDetailsBody()
e transmitauiState.value
para o argumentoitemUiState
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Observe as implementações de
ItemDetailsBody()
eItemInputForm()
. Você está transmitindo oitem
selecionado atual deItemDetailsBody()
paraItemDetails()
.
// 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()
)
//...
}
- Execute o app. Ao clicar em qualquer elemento da lista na tela Inventory, a tela Item Details será mostrada.
- Observe que a tela não está mais em branco. Ela mostra os detalhes da entidade extraídos do banco de dados do inventário.
- Toque no botão Sell. Nada acontece!
Na próxima seção, você vai implementar a funcionalidade do botão Sell.
7. Implementar a tela de detalhes do item
ui/item/ItemEditScreen.kt
A tela do editor de itens já foi fornecida a você como parte do código inicial.
Esse layout contém elementos combináveis de campo de texto para editar os detalhes de qualquer novo item de inventário.
O código do app ainda não é totalmente funcional. Por exemplo, na tela Item Details, quando você toca no botão Sell (vender), a Quantity in Stock (quantidade em estoque) não diminui. Quando você toca no botão Delete (excluir), o app mostra uma caixa de diálogo de confirmação. No entanto, ao selecionar o botão Yes (sim), o app não exclui o item.
Por fim, o botão FAB abre uma tela vazia Edit Item (editar item).
Nesta seção, você vai implementar as funcionalidades dos botões Sell, Delete e FAB.
8. Implementar a venda de itens
Nesta seção, você vai estender os recursos do app para implementar a funcionalidade de venda. Essa atualização envolve as tarefas abaixo:
- Adicione um teste para a função do DAO para atualizar uma entidade.
- Adicione uma função no
ItemDetailsViewModel
para reduzir a quantidade e atualizar a entidade no banco de dados do app. - Desative o botão Sell se a quantidade for igual a zero.
- No
ItemDaoTest.kt
, adicione uma função com o nomedaoUpdateItems_updatesItemsInDB()
sem parâmetros. Adicione as anotações@Test
e@Throws(Exception::class)
.
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- Defina a função e crie um bloco
runBlocking
. ChameaddTwoItemsToDb()
dentro dele.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- Atualize as duas entidades com valores diferentes, chamando
itemDao.update
.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
- Extraia as entidades com
itemDao.getAllItems()
. Compare-as com a entidade atualizada e faça a declaração.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- Verifique se a função concluída é semelhante a esta:
@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))
}
- Execute o teste e confira se ele é aprovado.
Adicionar uma função no ViewModel
- No
ItemDetailsViewModel.kt
, na classeItemDetailsViewModel
, adicione uma função com o nomereduceQuantityByOne()
sem parâmetros.
fun reduceQuantityByOne() {
}
- Na função, inicie uma corrotina com
viewModelScope.launch{}
.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
- No bloco
launch
, crie umval
com o nomecurrentItem
e o defina comouiState.value.toItem()
.
val currentItem = uiState.value.toItem()
O uiState.value
é do tipo ItemUiState
. Converta-o no tipo de entidade Item
com a função de extensão toItem
()
.
- Adicione uma instrução
if
para verificar se aquality
é maior que0
. - Chame
updateItem()
emitemsRepository
e transmita ocurrentItem
atualizado. Usecopy()
a fim de atualizar o valorquantity
para que a função fique assim:
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
- Volte para o arquivo
ItemDetailsScreen.kt
. - No elemento combinável
ItemDetailsScreen
, acesse a chamada de funçãoItemDetailsBody()
. - Na lambda
onSellItem
, chameviewModel.reduceQuantityByOne()
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Execute o app.
- Na tela Inventory, clique em um elemento de lista. Quando a tela Item Details aparecer, toque em Sell e observe que o valor da quantidade diminui em um.
- Na tela Item Details, toque continuamente no botão Sell até que a quantidade seja zero.
Quando a quantidade chegar a zero, toque em Sell novamente. Não há mudança visual porque a função reduceQuantityByOne()
verifica se a quantidade é maior que zero antes de atualizar a quantidade.
Para dar uma resposta melhor ao usuário, desative o botão Sell quando não houver nenhum item para venda.
- Na classe
ItemDetailsViewModel
, defina o valoroutOfStock
com base noit
.quantity
na transformaçãomap
.
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- Execute o app. O app desativa o botão Sell quando a quantidade de itens em estoque é igual a zero.
Parabéns por implementar o recurso Sell no app.
Excluir a entidade do item
Assim como na tarefa anterior, você precisa estender ainda mais os recursos do app implementando o recurso de exclusão. Esse recurso é muito mais fácil de implementar do que o recurso de venda. O processo envolve estas tarefas:
- Adicione um teste para a consulta de DAO de exclusão.
- Adicione uma função na classe
ItemDetailsViewModel
para excluir uma entidade do banco de dados. - Atualize o elemento combinável
ItemDetailsBody
.
Adicionar um teste do DAO
- No
ItemDaoTest.kt
, adicione um teste com o nomedaoDeleteItems_deletesAllItemsFromDB()
.
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
- Inicie uma corrotina com
runBlocking {}
.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- Adicione dois itens ao banco de dados e chame
itemDao.delete()
nesses dois itens para os excluir do banco de dados.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- Extraia as entidades do banco de dados e verifique se a lista está vazia. O teste concluído ficará assim:
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())
}
Adicionar a função de exclusão no ItemDetailsViewModel
- No
ItemDetailsViewModel
, adicione uma nova função com o nomedeleteItem()
, que não usa parâmetros e não retorna nada. - Na função
deleteItem()
, adicione uma chamada de funçãoitemsRepository.deleteItem()
e transmitauiState.value.
toItem
()
.
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
Nessa função, você converte o uiState
do tipo itemDetails
no tipo de entidade Item
usando a função de extensão toItem
()
.
- No elemento combinável
ui/item/ItemDetailsScreen
, adicione umval
com o nomecoroutineScope
e o defina comorememberCoroutineScope()
. Essa abordagem retorna um escopo de corrotina vinculado à composição em que ele é chamado (elemento combinávelItemDetailsScreen
).
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Role até a função
ItemDetailsBody()
. - Inicie uma corrotina com
coroutineScope
dentro da lambdaonDelete
. - No bloco
launch
, chame o métododeleteItem()
noviewModel
.
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- Depois de excluir o item, volte à tela de inventário.
- Chame
navigateBack()
depois da chamada de funçãodeleteItem()
.
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- Ainda no arquivo
ItemDetailsScreen.kt
, role até a funçãoItemDetailsBody()
.
Essa função faz parte do código inicial. Esse elemento combinável mostra uma caixa de diálogo de alerta para receber a confirmação do usuário antes de excluir o item e chama a função deleteItem()
quando você toca em 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()
},
//...
)
}
}
}
Quando você toca em No (não), o app fecha a caixa de diálogo de alerta. A função showConfirmationDialog()
mostra este alerta:
- Execute o app.
- Selecione um elemento da lista na tela Inventory.
- Na tela Item Details, toque em Delete.
- Toque em Yes na caixa de diálogo de alerta. O app voltará para a tela Inventory.
- Confirme se a entidade excluída não está mais no banco de dados do app.
Parabéns por implementar o recurso de exclusão.
Editar a entidade do item
Assim como nas seções anteriores, nesta, você vai adicionar outra melhoria de recurso ao app que edita uma entidade de item.
Confira um resumo das etapas para editar uma entidade no banco de dados do app:
- Adicione um teste à consulta do DAO de recebimento de item de teste.
- Preencha os campos de texto e a tela Edit Item com os detalhes da entidade.
- Atualize a entidade no banco de dados usando o Room.
Adicionar um teste do DAO
- No
ItemDaoTest.kt
, adicione um teste com o nomedaoGetItem_returnsItemFromDB()
.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- Defina a função. Na corrotina, adicione um item ao banco de dados.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
- Extraia a entidade do banco de dados usando a função
itemDao.getItem()
e a defina como umval
com o nomeitem
.
val item = itemDao.getItem(1)
- Compare o valor real com o valor extraído e faça a declaração usando
assertEquals()
. O teste concluído vai ficar assim:
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- Execute o teste e confirme se ele foi aprovado.
Preencher campos de texto
Se você executar o app, acesse a tela Item Details e clique no FAB. O título da tela agora é Edit Item. No entanto, todos os campos estão vazios. Nesta etapa, você vai preencher os campos de texto da tela Edit Item com as informações da entidade.
- No
ItemDetailsScreen.kt
, role até o elemento combinávelItemDetailsScreen
. - No
FloatingActionButton()
, mude o argumentoonClick
para incluiruiState.value.itemDetails.id
, que é oid
da entidade selecionada. Use esseid
para extrair os detalhes da entidade.
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
- Na classe
ItemEditViewModel
, adicione um blocoinit
.
init {
}
- No bloco
init
, inicie uma corrotina com oviewModelScope
.
launch
.
import kotlinx.coroutines.launch
viewModelScope.launch { }
- No bloco
launch
, extraia os detalhes da entidade comitemsRepository.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)
}
}
Nesse bloco de inicialização, você adiciona um filtro para retornar um fluxo que contém apenas valores não nulos. Com toItemUiState()
, você converte a entidade item
em ItemUiState
. Transmita o valor actionEnabled
como true
para ativar o botão Save.
Para resolver o erro Unresolved reference: itemsRepository
, é necessário transmitir o ItemsRepository
como uma dependência ao modelo de visualização.
- Adicione um parâmetro construtor à classe
ItemEditViewModel
.
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
- No arquivo
AppViewModelProvider.kt
, no inicializadorItemEditViewModel
, adicione o objetoItemsRepository
como um argumento.
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Execute o app.
- Acesse Item Details e toque no FAB .
- Observe que os campos são preenchidos com os detalhes do item.
- Edite a quantidade em estoque ou qualquer outro campo e toque no botão Save.
Nada acontece! Isso ocorre porque você não está atualizando a entidade no banco de dados do app. Isso será corrigido na próxima seção.
Atualizar a entidade usando o Room
Nesta tarefa final, você vai adicionar as partes finais do código para implementar o recurso de atualização. Você vai definir as funções necessárias no ViewModel e usá-las na ItemEditScreen
.
É hora de programar novamente.
- Na classe
ItemEditViewModel
, adicione uma função com o nomeupdateUiState()
, que usa um objetoItemUiState
e não retorna nada. Essa função atualiza oitemUiState
com novos valores inseridos pelo usuário.
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
Nessa função, você atribui o itemDetails
transmitido ao itemUiState
e atualiza o valor isEntryValid
. O app vai ativar o botão Salvar se itemDetails
for true
. Defina esse valor como true
somente se a entrada inserida pelo usuário for válida.
- Acesse o arquivo
ItemEditScreen.kt
. - No elemento combinável
ItemEditScreen
, role a tela para baixo até a chamada de funçãoItemEntryBody()
. - Defina o valor do argumento
onItemValueChange
como a nova funçãoupdateUiState
.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- Execute o app.
- Acesse a tela Edit Item.
- Deixe um dos valores da entidade vazios para que seja inválido. Observe como o botão Save é desativado automaticamente.
- Volte para a classe
ItemEditViewModel
e adicione uma funçãosuspend
com o nomeupdateItem()
, que não retorna nada. Use essa função para salvar a entidade atualizada no banco de dados do Room.
suspend fun updateItem() {
}
- Dentro da função
getUpdatedItemEntry()
, adicione uma condiçãoif
para validar a entrada do usuário chamando a funçãovalidateInput()
. - Chame a função
updateItem()
noitemsRepository
, transmitindo oitemUiState.itemDetails.
toItem
()
. As entidades que podem ser adicionadas ao banco de dados do Room precisam ser do tipoItem
. A função concluída vai ficar assim:
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
- Volte para o elemento combinável
ItemEditScreen
. Você precisa de um escopo de corrotinas para chamar a funçãoupdateItem()
. Crie um val com o nomecoroutineScope
e o defina comorememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Na chamada de função
ItemEntryBody()
, atualize o argumento de funçãoonSaveClick
para iniciar uma corrotina nocoroutineScope
. - No bloco
launch
, chameupdateItem()
noviewModel
e navegue de volta.
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
A chamada de função ItemEntryBody()
concluída tem esta aparência:
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- Execute o app e tente editar os itens do inventário. Agora você pode editar qualquer item no banco de dados do app Inventory.
Parabéns por criar seu primeiro app que usa o Room para gerenciar o banco de dados.
9. Código da solução
O código da solução para este codelab está no repositório do GitHub e na ramificação mostrados abaixo:
10. Saiba mais
Documentação do desenvolvedor Android
- Depurar seu banco de dados com o Database Inspector
- Salvar dados em um banco de dados local usando Room
- Testar e depurar o banco de dados | Desenvolvedores Android
Referências do Kotlin