Ler e atualizar dados com o Room

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.

Tela "Add Item" com detalhes do item preenchidos.

Tela de um smartphone com o inventário vazio

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.

Tela de um smartphone com itens de um inventário

Tutorial da função combinável HomeScreen

  • Abra o arquivo ui/home/HomeScreen.kt e observe a função combinável HomeScreen().
@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 7b1535d90ee957fa.png
  • 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: o viewModelScope define o ciclo de vida do StateFlow. Quando o viewModelScope é cancelado, o StateFlow também é.
  • started: o pipeline só fica ativo quando a IU está visível. O SharingStarted.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 o TIMEOUT_MILLIS para o método SharingStarted.WhileSubscribed().
  • initialValue: define o valor inicial do fluxo de estado como HomeUiState().

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.

  1. Abra o arquivo ui/home/HomeViewModel.kt, que contém uma constante TIMEOUT_MILLIS e uma classe de dados HomeUiState 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())
  1. Na classe HomeViewModel, declare um val com o nome homeUiState e o tipo StateFlow<HomeUiState>. Você vai resolver o erro de inicialização em breve.
val homeUiState: StateFlow<HomeUiState>
  1. Chame getAllItemsStream() em itemsRepository e o atribua ao homeUiState 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.

  1. Adicione um parâmetro construtor do tipo ItemsRepository à classe HomeViewModel.
import com.example.inventory.data.ItemsRepository

class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
  1. No arquivo ui/AppViewModelProvider.kt, no inicializador HomeViewModel, transmita o objeto ItemsRepository conforme mostrado.
initializer {
    HomeViewModel(inventoryApplication().container.itemsRepository)
}
  1. 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.

  1. Use o operador stateIn para converter o Flow em um StateFlow. O StateFlow é 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()
        )
  1. 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.

  1. No arquivo HomeScreen.kt, na função combinável HomeScreen, adicione e inicialize um novo parâmetro de função do tipo HomeViewModel.
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)
)
  1. Na função combinável HomeScreen, adicione um val com o nome homeUiState para coletar o estado da interface do HomeViewModel. Você vai usar collectAsState(), que coleta valores desse StateFlow (link em inglês) e representa o valor mais recente em State.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

val homeUiState by viewModel.homeUiState.collectAsState()
  1. Atualize a chamada de função HomeBody() e transmita homeUiState.itemList para o parâmetro itemList.
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier.padding(innerPadding)
)
  1. 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.

Tela de um smartphone com itens de um inventário

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.

  1. 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")
  1. 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.

9121189f4a0d2613.png

  1. Selecione androidTest/kotlin no menu pop-up New Directory.

fba4ed57c7589f7f.png

  1. Crie uma classe do Kotlin com o nome ItemDaoTest.kt.
  2. Adicione a anotação @RunWith(AndroidJUnit4::class) à classe ItemDaoTest. 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 {
}
  1. Dentro da classe, adicione variáveis var particulares dos tipos ItemDao e InventoryDatabase.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao

private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
  1. 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.
  2. 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.

  1. 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()
}
  1. 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)
  1. 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)
}
  1. Crie um teste para inserir um único item no banco de dados, insert(). Dê o nome daoInsert_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.

  1. Execute o teste e confira se ele é aprovado.

2f0ddde91781d6bd.png

8f66e03d03aac31a.png

  1. Crie outro teste para getAllItems() no banco de dados. Dê o nome daoGetAllItems_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.

de7761a894d1b2ab.png

  1. Na função combinável HomeScreen, observe a chamada de função HomeBody(). O navigateToItemUpdate está sendo transmitido para o parâmetro onItemClick, 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()
)
  1. Abra ui/navigation/InventoryNavGraph.kt e observe o parâmetro navigateToItemUpdate na função combinável HomeScreen. 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.

  1. Clique em qualquer item da lista de inventário para conferir a tela de detalhes com campos vazios.

Tela de detalhes do item com dados vazios

Para preencher os campos de texto com detalhes do item, é necessário coletar o estado da interface em ItemDetailsScreen().

  1. No UI/Item/ItemDetailsScreen.kt, adicione um novo parâmetro do tipo ItemDetailsViewModel ao elemento combinável ItemDetailsScreen 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)
)
  1. Dentro do elemento combinável ItemDetailsScreen(), crie um val com o nome uiState para coletar o estado da interface. Use collectAsState() para coletar uiState StateFlow e representar o valor mais recente usando State. O Android Studio mostra um erro de referência não resolvido.
import androidx.compose.runtime.collectAsState

val uiState = viewModel.uiState.collectAsState()
  1. Para resolver o erro, crie um val chamado uiState do tipo StateFlow<ItemDetailsUiState> na classe ItemDetailsViewModel.
  2. Extraia os dados do repositório de itens e mapeie-os para ItemDetailsUiState usando a função de extensão toItemDetails(). A função de extensão Item.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()
             )
  1. Transmita ItemsRepository ao ItemDetailsViewModel para resolver o erro Unresolved reference: itemsRepository.
class ItemDetailsViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
    ) : ViewModel() {
  1. No ui/AppViewModelProvider.kt, atualize o inicializador para o ItemDetailsViewModel, conforme mostrado neste snippet de código:
initializer {
    ItemDetailsViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Volte para o ItemDetailsScreen.kt e observe que o erro na função combinável ItemDetailsScreen() foi resolvido.
  2. Na função combinável ItemDetailsScreen(), atualize a chamada de função ItemDetailsBody() e transmita uiState.value para o argumento itemUiState.
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = {  },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Observe as implementações de ItemDetailsBody() e ItemInputForm(). Você está transmitindo o item selecionado atual de ItemDetailsBody() para ItemDetails().
// 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()
         )

      //...
    }
  1. Execute o app. Ao clicar em qualquer elemento da lista na tela Inventory, a tela Item Details será mostrada.
  2. 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.

Tela de detalhes do item com itens válidos

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

Editar layout dos itens com o nome, o preço e a quantidade em estoque

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.

pop-up de confirmação de exclusão de item

Por fim, o botão FAB aad0ce469e4a3a12.png abre uma tela vazia Edit Item (editar item).

Tela do editor de itens com campos vazios

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.
  1. No ItemDaoTest.kt, adicione uma função com o nome daoUpdateItems_updatesItemsInDB() sem parâmetros. Adicione as anotações @Test e @Throws(Exception::class).
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
  1. Defina a função e crie um bloco runBlocking. Chame addTwoItemsToDb() dentro dele.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
}
  1. 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))
  1. 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))
  1. 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))
}
  1. Execute o teste e confira se ele é aprovado.

Adicionar uma função no ViewModel

  1. No ItemDetailsViewModel.kt, na classe ItemDetailsViewModel, adicione uma função com o nome reduceQuantityByOne() sem parâmetros.
fun reduceQuantityByOne() {
}
  1. Na função, inicie uma corrotina com viewModelScope.launch{}.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope

viewModelScope.launch {
}
  1. No bloco launch, crie um val com o nome currentItem e o defina como uiState.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().

  1. Adicione uma instrução if para verificar se a quality é maior que 0.
  2. Chame updateItem() em itemsRepository e transmita o currentItem atualizado. Use copy() a fim de atualizar o valor quantity 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))
       }
    }
}
  1. Volte para o arquivo ItemDetailsScreen.kt.
  2. No elemento combinável ItemDetailsScreen, acesse a chamada de função ItemDetailsBody().
  3. Na lambda onSellItem, chame viewModel.reduceQuantityByOne().
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Execute o app.
  2. 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.

A tela de detalhes do item diminui a quantidade em um quando o botão de venda é tocado

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

Tela de detalhes do item com quantidade em estoque como 0

Para dar uma resposta melhor ao usuário, desative o botão Sell quando não houver nenhum item para venda.

  1. Na classe ItemDetailsViewModel, defina o valor outOfStock com base no it.quantity na transformação map.
val uiState: StateFlow<ItemDetailsUiState> =
    itemsRepository.getItemStream(itemId)
        .filterNotNull()
        .map {
            ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
        }.stateIn(
            //...
        )
  1. Execute o app. O app desativa o botão Sell quando a quantidade de itens em estoque é igual a zero.

Tela de detalhes do item com o botão de venda desativado

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

  1. No ItemDaoTest.kt, adicione um teste com o nome daoDeleteItems_deletesAllItemsFromDB().
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
  1. Inicie uma corrotina com runBlocking {}.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
  1. 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)
  1. 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

  1. No ItemDetailsViewModel, adicione uma nova função com o nome deleteItem(), que não usa parâmetros e não retorna nada.
  2. Na função deleteItem(), adicione uma chamada de função itemsRepository.deleteItem() e transmita uiState.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().

  1. No elemento combinável ui/item/ItemDetailsScreen, adicione um val com o nome coroutineScope e o defina como rememberCoroutineScope(). Essa abordagem retorna um escopo de corrotina vinculado à composição em que ele é chamado (elemento combinável ItemDetailsScreen).
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Role até a função ItemDetailsBody().
  2. Inicie uma corrotina com coroutineScope dentro da lambda onDelete.
  3. No bloco launch, chame o método deleteItem() no viewModel.
import kotlinx.coroutines.launch

ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = {
        coroutineScope.launch {
           viewModel.deleteItem()
    }
    modifier = modifier.padding(innerPadding)
)
  1. Depois de excluir o item, volte à tela de inventário.
  2. Chame navigateBack() depois da chamada de função deleteItem().
onDelete = {
    coroutineScope.launch {
        viewModel.deleteItem()
        navigateBack()
    }
  1. Ainda no arquivo ItemDetailsScreen.kt, role até a função ItemDetailsBody().

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:

pop-up de confirmação de exclusão de item

  1. Execute o app.
  2. Selecione um elemento da lista na tela Inventory.
  3. Na tela Item Details, toque em Delete.
  4. Toque em Yes na caixa de diálogo de alerta. O app voltará para a tela Inventory.
  5. 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.

Tela de detalhes do item com a janela de diálogo de alerta.

Tela de um smartphone mostrando a lista de inventário sem o item excluído

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

  1. No ItemDaoTest.kt, adicione um teste com o nome daoGetItem_returnsItemFromDB().
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
  1. Defina a função. Na corrotina, adicione um item ao banco de dados.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
}
  1. Extraia a entidade do banco de dados usando a função itemDao.getItem() e a defina como um val com o nome item.
val item = itemDao.getItem(1)
  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)
}
  1. 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.

A tela de detalhes do item diminui a quantidade em um quando o botão de venda é tocado

Tela do editor de itens com campos vazios

  1. No ItemDetailsScreen.kt, role até o elemento combinável ItemDetailsScreen.
  2. No FloatingActionButton(), mude o argumento onClick para incluir uiState.value.itemDetails.id, que é o id da entidade selecionada. Use esse id para extrair os detalhes da entidade.
FloatingActionButton(
    onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
    modifier = /*...*/
)
  1. Na classe ItemEditViewModel, adicione um bloco init.
init {

}
  1. No bloco init, inicie uma corrotina com o viewModelScope.launch.
import kotlinx.coroutines.launch

viewModelScope.launch { }
  1. No bloco launch, extraia os detalhes da entidade com itemsRepository.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.

  1. Adicione um parâmetro construtor à classe ItemEditViewModel.
class ItemEditViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
)
  1. No arquivo AppViewModelProvider.kt, no inicializador ItemEditViewModel, adicione o objeto ItemsRepository como um argumento.
initializer {
    ItemEditViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Execute o app.
  2. Acesse Item Details e toque no FAB 73b88f16638608f0.png.
  3. Observe que os campos são preenchidos com os detalhes do item.
  4. 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.

A tela de detalhes do item diminui a quantidade em um quando o botão de venda é tocado

Tela do editor de itens com campos vazios

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.

  1. Na classe ItemEditViewModel, adicione uma função com o nome updateUiState(), que usa um objeto ItemUiState e não retorna nada. Essa função atualiza o itemUiState 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.

  1. Acesse o arquivo ItemEditScreen.kt.
  2. No elemento combinável ItemEditScreen, role a tela para baixo até a chamada de função ItemEntryBody().
  3. Defina o valor do argumento onItemValueChange como a nova função updateUiState.
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = modifier.padding(innerPadding)
)
  1. Execute o app.
  2. Acesse a tela Edit Item.
  3. Deixe um dos valores da entidade vazios para que seja inválido. Observe como o botão Save é desativado automaticamente.

Tela de detalhes do item com o botão de venda ativado

Tela do editor de itens o botão "Save" e todos os campos de texto ativados

Tela do editor de itens com o botão "Save" desativado

  1. Volte para a classe ItemEditViewModel e adicione uma função suspend com o nome updateItem(), que não retorna nada. Use essa função para salvar a entidade atualizada no banco de dados do Room.
suspend fun updateItem() {
}
  1. Dentro da função getUpdatedItemEntry(), adicione uma condição if para validar a entrada do usuário chamando a função validateInput().
  2. Chame a função updateItem() no itemsRepository, transmitindo o itemUiState.itemDetails.toItem(). As entidades que podem ser adicionadas ao banco de dados do Room precisam ser do tipo Item. A função concluída vai ficar assim:
suspend fun updateItem() {
    if (validateInput(itemUiState.itemDetails)) {
        itemsRepository.updateItem(itemUiState.itemDetails.toItem())
    }
}
  1. Volte para o elemento combinável ItemEditScreen. Você precisa de um escopo de corrotinas para chamar a função updateItem(). Crie um val com o nome coroutineScope e o defina como rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Na chamada de função ItemEntryBody(), atualize o argumento de função onSaveClick para iniciar uma corrotina no coroutineScope.
  2. No bloco launch, chame updateItem() no viewModel 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)
)
  1. Execute o app e tente editar os itens do inventário. Agora você pode editar qualquer item no banco de dados do app Inventory.

Tela do editor de itens editada

Tela de detalhes do item atualizada

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

Referências do Kotlin