Ler e atualizar dados com o Room

Nos codelabs anteriores, você aprendeu a usar uma biblioteca de persistência Room, que é uma camada de abstração sobre um banco de dados SQLite para armazenar dados de apps. Neste codelab, você adicionará mais recursos ao app Inventory e aprenderá a ler, exibir, atualizar e excluir dados do banco de dados SQLite usando o Room. Você usará uma RecyclerView para exibir os dados do banco de dados e atualizá-los automaticamente quando os dados subjacentes mudarem.

Pré-requisitos

  • Saber criar e interagir com o banco de dados SQLite usando a biblioteca Room.
  • Saber criar uma entidade, um DAO e classes de banco de dados.
  • Saber usar um objeto de acesso a dados (DAO, na sigla em inglês) para mapear funções Kotlin para consultas SQL.
  • Saber exibir itens de lista em uma RecyclerView.
  • Ter concluído o codelab anterior desta unidade, Como persistir dados com o Room.

O que você aprenderá

  • Ler e exibir entidades de um banco de dados SQLite.
  • Atualizar e excluir entidades de um banco de dados SQLite usando a biblioteca Room.

O que você criará

  • Você criará um app chamado Inventory, que exibe uma lista de itens de inventário. O app poderá atualizar, editar e excluir itens do banco de dados do app usando o Room.

Este codelab usa o código da solução do app Inventory do codelab anterior como o código inicial. O app inicial já salva dados usando a biblioteca de persistência Room. O usuário pode adicionar dados ao banco de dados do app usando a tela Add Item.

Observação: a versão atual do app inicial não exibe a data armazenada no banco de dados.

771c6a677ecd96c7.png

Neste codelab, você ampliará o app para ler e exibir os dados, além de atualizar e excluir entidades do banco de dados usando a biblioteca Room.

Fazer o download do código inicial para este codelab

O código inicial usado é igual ao código da solução do codelab anterior.

Para encontrar o código deste codelab e abri-lo no Android Studio, faça o seguinte.

Buscar o código

  1. Clique no URL fornecido. Isso abrirá a página do GitHub referente ao projeto em um navegador.
  2. Na página do GitHub do projeto, clique no botão Code, que exibirá uma caixa de diálogo.

5b0a76c50478a73f.png

  1. Na caixa de diálogo, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
  2. Localize o arquivo no computador, que provavelmente está na pasta Downloads.
  3. Clique duas vezes no arquivo ZIP para descompactá-lo. Isso criará uma nova pasta com os arquivos do projeto.

Abrir o projeto no Android Studio

  1. Inicie o Android Studio.
  2. Na janela Welcome to Android Studio, clique em Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Observação: caso o Android Studio já esteja aberto, selecione a opção File > New > Import Project.

21f3eec988dcfbe9.png

  1. Na caixa de diálogo Import Project, vá até a pasta do projeto descompactada, que provavelmente está na pasta Downloads.
  2. Clique duas vezes nessa pasta do projeto.
  3. Aguarde o Android Studio abrir o projeto.
  4. Clique no botão Run 11c34fc5e516fb1c.png para criar e executar o app. Confira se ele funciona da forma esperada.
  5. Procure os arquivos do projeto na janela de ferramentas Project para ver como o app foi implementado.

Nesta tarefa, você adicionará uma RecyclerView ao app para exibir os dados armazenados no banco de dados.

Adicionar função auxiliar para definir o formato dos preços

Veja abaixo uma captura de tela do app final.

d6e7b7b9f12e7a16.png

O preço é exibido no formato de moeda. Para converter um valor para o formato de moeda desejado, adicione uma função de extensão à classe Item.

Funções de extensão

O Kotlin permite estender uma classe com nova função sem que seja necessário herdá-la da classe ou modificar a definição existente. Isso significa que você pode adicionar funções a uma classe existente sem precisar acessar o código-fonte. Isso é feito usando declarações especiais chamadas extensões (link em inglês).

Por exemplo, você pode escrever novas funções para uma classe de uma biblioteca de terceiros que não pode ser modificada. Essas funções estão disponíveis para serem chamadas da forma normal, como se fossem métodos da classe original. Elas são chamadas de funções de extensão. Também existem propriedades de extensão, que permitem definir novas propriedades para classes existentes, mas elas não fazem parte do escopo deste codelab.

As funções de extensão não modificam a classe, mas permitem que você use a notação de ponto ao chamar a função em objetos dessa classe.

No snippet de código a seguir, por exemplo, há uma classe chamada Square. Essa classe tem uma propriedade para a parte lateral e uma função para calcular a área do quadrado. Na função de extensão Square.perimeter(), o nome da função é prefixado com a classe em que ela opera. Dentro da função, é possível referenciar as propriedades públicas da classe Square.

Observe o uso da função de extensão dentro da função main(). A função de extensão criada, perimeter(), é chamada como uma função normal dentro dessa classe Square.

Exemplo:

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

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

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

Nesta etapa, você definirá o formato do preço do item como uma string de moeda. Em geral, não é recomendável mudar uma classe de entidade que representa dados somente para formatá-los. Consulte o princípio de responsabilidade única (link em inglês) para ver mais informações sobre esse assunto. Por esse motivo, adicione uma função de extensão.

  1. Em Item.kt, abaixo da definição de classe, adicione uma função de extensão chamada Item.getFormattedPrice(), que não usa nenhum parâmetro e retorna uma String. Observe o nome da classe e a notação de ponto no nome da função.
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

Importe java.text.NumberFormat quando solicitado pelo Android Studio.

Adicionar o ListAdapter

Nesta etapa, você adicionará um adaptador de lista à RecyclerView. Como você já aprendeu como implementar o adaptador nos codelabs anteriores, as instruções estão resumidas abaixo. Para sua conveniência e para auxiliar o entendimento dos conceitos do Room neste codelab, o arquivo ItemListAdapter completo está no final desta etapa.

  1. No pacote com.example.inventory, adicione uma classe do Kotlin chamada ItemListAdapter. Transmita uma função chamada onItemClicked() como um parâmetro construtor que usa um objeto Item como parâmetro.
  2. Mude a assinatura da classe ItemListAdapter para estender ListAdapter. Transmita Item e ItemListAdapter.ItemViewHolder como parâmetros.
  3. Adicione o parâmetro construtor DiffCallback. O ListAdapter usará isso para descobrir o que mudou na lista.
  4. Modifique os métodos obrigatórios onCreateViewHolder() e onBindViewHolder().
  5. O método onCreateViewHolder() retorna um novo ViewHolder quando a RecyclerView precisa de um.
  6. Dentro do método onCreateViewHolder(), crie uma nova View e infle-a no arquivo de layout item_list_item.xml usando a classe de vinculação gerada automaticamente, ItemListItemBinding.
  7. Implemente o método onBindViewHolder(). Busque o item que está selecionado usando o método getItem(), transmitindo a posição dele.
  8. Defina o listener de clique na itemView e chame a função onItemClicked() dentro do listener.
  9. Defina a classe ItemViewHolder e estenda-a no RecyclerView.ViewHolder. Modifique a função bind() e transmita o objeto Item.
  10. Defina um objeto complementar. Dentro dele, defina um val do tipo DiffUtil.ItemCallback<Item>(), chamado DiffCallback. Modifique os métodos obrigatórios areItemsTheSame() e areContentsTheSame() e defina-os.

A classe finalizada ficará assim:

package com.example.inventory

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

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

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

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

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

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

       fun bind(item: Item) {

       }
   }

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

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

Observe a tela da lista de inventário do app finalizado, que é o app da solução do final deste codelab. Cada elemento da lista exibe o nome do item do inventário, o preço no formato de moeda e a disponibilidade atual no estoque. Nas etapas anteriores, você usou o arquivo de layout item_list_item.xml com três TextViews para criar linhas. Na próxima etapa, você vinculará as informações da entidade a essas TextViews.

9c416f2fbf1e5ae2.png

  1. Em ItemListAdapter.kt, implemente a função bind() na classe ItemViewHolder. Vincule a TextView itemName a item.itemName. Retorne o preço no formato de moeda usando a função de extensão getFormattedPrice() e vincule-o à TextView itemPrice. Converta o valor de quantityInStock em String e vincule-o à TextView itemQuantity. O método completo ficará assim:
fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemQuantity.text = item.quantityInStock.toString()
   }
}

Quando solicitado pelo Android Studio, importe com.example.inventory.data.getFormattedPrice.

Usar o ListAdapter

Nesta tarefa, você atualizará o InventoryViewModel e o ItemListFragment para exibir os detalhes do item na tela, usando o adaptador de lista criado na etapa anterior.

  1. No início da classe InventoryViewModel, crie um val chamado allItems, do tipo LiveData<List<Item>>, para os itens do banco de dados. Não se preocupe com o erro exibido, porque ele será corrigido em breve.
val allItems: LiveData<List<Item>>

Importe androidx.lifecycle.LiveData quando solicitado pelo Android Studio.

  1. Chame getItems() em itemDao e atribua-o a allItems. A função getItems() retorna um Flow. Para consumir os dados como um valor LiveData, use a função asLiveData(). A definição concluída ficará assim:
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

Importe androidx.lifecycle.asLiveData quando solicitado pelo Android Studio.

  1. No ItemListFragment, no início da classe, declare uma propriedade imutável private chamada viewModel, do tipo InventoryViewModel. Use o delegado by para entregar a inicialização da propriedade à classe activityViewModels. Transmita o construtor InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importe androidx.fragment.app.activityViewModels quando solicitado pelo Android Studio.

  1. Ainda no ItemListFragment, role para executar a função onViewCreated(). Abaixo da chamada para super.onViewCreated(), declare um val chamado adapter. Inicialize a nova propriedade adapter usando o construtor padrão ItemListAdapter{}, que não transmite nada.
  2. Vincule o adapter recém-criado à recyclerView da seguinte maneira:
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
  1. Ainda dentro do onViewCreated(), após a configuração do adaptador, anexe um observador a allItems para ouvir as mudanças de dados.
  2. Dentro do observador, chame submitList() no adapter e transmita a nova lista. Isso atualizará a RecyclerView com os novos itens da lista.
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
   items.let {
       adapter.submitList(it)
   }
}
  1. Verifique se o método onViewCreated() completo ficou semelhante ao exemplo abaixo. Execute o app. Se houver itens salvos no banco de dados do app, a lista de inventário será exibida. Adicione alguns itens de inventário ao banco de dados do app caso a lista esteja vazia.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

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

9c416f2fbf1e5ae2.png

Nesta tarefa, você lerá e exibirá as informações da entidade na tela Detalhes do item. Você usará a chave primária (o item id) para ler as informações, como nome, preço e quantidade do banco de dados do app de inventário, e exibi-las na tela Detalhes do item, usando o arquivo de layout fragment_item_detail.xml. Esse arquivo fragment_item_detail.xml já foi criado para você, contendo três TextViews que exibem os detalhes do item.

d699618f5d9437df.png

Você implementará as seguintes etapas nesta tarefa:

  • Adicione um gerenciador de cliques à RecyclerView para navegar no app até a tela Detalhes do item.
  • No fragmento ItemListFragment, recupere os dados do banco de dados e exiba-os.
  • Vincule as TextViews aos dados do ViewModel.

Adicionar um gerenciador de cliques

  1. No ItemListFragment, role até a função onViewCreated() para atualizar a definição do adaptador.
  2. Adicione um lambda como parâmetro construtor ao ItemListAdapter{}.
val adapter = ItemListAdapter {
}
  1. Dentro do lambda, crie um val chamado action. O erro de inicialização será corrigido em breve.
val adapter = ItemListAdapter {
    val action
}
  1. Chame o método actionItemListFragmentToItemDetailFragment() no ItemListFragmentDirections transmitindo o item id. Atribua o objeto NavDirections retornado à action.
val adapter = ItemListAdapter {
   val action =    ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
  1. Abaixo da definição action, recupere uma instância do NavController usando this.findNavController() e chame navigate() nela, transmitindo a action. A definição do adaptador ficará assim:
val adapter = ItemListAdapter {
   val action =   ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
   this.findNavController().navigate(action)
}
  1. Execute o app. Clique em um item na RecyclerView. O app navegará para a tela Item Details. Observe que os detalhes estão em branco. Nada acontecerá ao tocar nos botões.

196553111ee69beb.png

Nas próximas etapas, você exibirá as informações da entidade na tela Item Details e adicionará as funções dos botões "sell" e "delete".

Recuperar detalhes do item

Nesta etapa, você adicionará uma nova função ao InventoryViewModel para recuperar os detalhes dos itens do banco de dados com base no item id. Na próxima etapa, você usará essa função para exibir as informações da entidade na tela Item Details.

  1. No InventoryViewModel, adicione uma função chamada retrieveItem(), que aceita um Int para o ID do item e retorna um LiveData<Item>. O erro da expressão de retorno será corrigido em breve.
fun retrieveItem(id: Int): LiveData<Item> {
}
  1. Dentro da nova função, chame getItem() no itemDao, transmitindo o parâmetro id. A função getItem() retorna um Flow. Para consumir o valor do Flow como LiveData, chame a função asLiveData() e use-a como o retorno da função retrieveItem(). A função concluída ficará assim:
fun retrieveItem(id: Int): LiveData<Item> {
   return itemDao.getItem(id).asLiveData()
}

Vincular dados a TextViews

Nesta etapa, você criará uma instância do ViewModel em ItemDetailFragment e vinculará os dados do ViewModel às TextViews na tela Item Details. Você também anexará um observador aos dados no ViewModel para manter sua lista de inventário atualizada na tela, caso os dados do banco de dados mudem.

  1. No ItemDetailFragment, adicione uma propriedade mutável chamada item da entidade do tipo Item. Você usará essa propriedade para armazenar informações sobre uma única entidade. Essa propriedade será inicializada mais tarde. Portanto, acrescente o prefixo lateinit.
lateinit var item: Item

Importe com.example.inventory.data.Item quando solicitado pelo Android Studio.

  1. No início da classe ItemDetailFragment, declare uma propriedade imutável private chamada viewModel, do tipo InventoryViewModel. Use o delegado by para entregar a inicialização da propriedade à classe activityViewModels. Transmita o construtor InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importe androidx.fragment.app.activityViewModels, se solicitado pelo Android Studio.

  1. Ainda no ItemDetailFragment, crie uma função private chamada bind(), que usa uma instância da entidade Item como o parâmetro e não retorna nada.
private fun bind(item: Item) {
}
  1. Implemente a função bind(), de forma semelhante ao que foi feito no ItemListAdapter. Defina a propriedade text da TextView itemName como item.itemName. Chame getFormattedPrice() na propriedade item para determinar o formato do preço e defina-o como a propriedade text da TextView itemPrice. Converta o quantityInStock para String e defina-o como a propriedade text da TextView itemQuantity.
private fun bind(item: Item) {
   binding.itemName.text = item.itemName
   binding.itemPrice.text = item.getFormattedPrice()
   binding.itemCount.text = item.quantityInStock.toString()
}
  1. Atualize a função bind() para usar a função de escopo apply{} no bloco de código, conforme mostrado abaixo.
private fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemCount.text = item.quantityInStock.toString()
   }
}
  1. Ainda no ItemDetailFragment, modifique onViewCreated().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
}
  1. Em uma das etapas anteriores, você transmitiu o ID do item como um argumento de navegação para o ItemDetailFragment, usando o ItemListFragment. Em onViewCreated(), abaixo da chamada para a função de superclasse, crie uma variável imutável chamada id. Recupere e atribua o argumento de navegação a essa nova variável.
val id = navigationArgs.itemId
  1. Agora, você usará essa variável id para recuperar os detalhes do item. Ainda dentro de onViewCreated(), chame a função retrieveItem() no viewModel, transmitindo o id. Anexe um observador ao valor retornado transmitindo o viewLifecycleOwner e um lambda.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. No lambda, transmita o selectedItem como o parâmetro que contém a entidade Item recuperada do banco de dados. No corpo da função lambda, atribua o valor selectedItem ao item. Chame a função bind() transmitindo o item. A função concluída ficará assim:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
       item = selectedItem
       bind(item)
   }
}
  1. Execute o app. Clique em qualquer elemento da lista na tela Inventory. A tela Item Details será exibida. A tela não estará mais em branco e exibirá as informações de entidades recuperadas do banco de dados do inventário.

  1. Toque nos botões Sell, Delete e no FAB. Nada acontecerá. Nas próximas tarefas, você implementará as funções desses botões.

Nesta tarefa, você estenderá os recursos do app e implementará a função de venda. Veja uma síntese das instruções para esta etapa a seguir.

  • Adicione uma função no ViewModel para atualizar uma entidade.
  • Crie um novo método para reduzir a quantidade e atualize a entidade no banco de dados do app.
  • Anexe um listener de clique ao botão Sell.
  • Desative o botão Sell se a quantidade for igual a zero.

Vamos começar a programar:

  1. No InventoryViewModel, adicione uma função privada chamada updateItem(), que usa uma instância da classe de entidade Item e não retorna nada.
private fun updateItem(item: Item) {
}
  1. Implemente o novo método updateItem(). Para chamar o método de suspensão update() da classe ItemDao, inicie uma corrotina usando o viewModelScope. No bloco de inicialização, faça uma chamada para a função update() no itemDao transmitindo o item. O método concluído ficará assim:
private fun updateItem(item: Item) {
   viewModelScope.launch {
       itemDao.update(item)
   }
}
  1. Ainda dentro do InventoryViewModel, adicione outro método chamado sellItem(), que usa uma instância da classe de entidade Item e não retorna nada.
fun sellItem(item: Item) {
}
  1. Dentro da função sellItem(), adicione uma condição if para verificar se item.quantityInStock é maior que 0.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
   }
}

No bloco if, a função copy() (link em inglês) será usada para que a classe de dados atualize a entidade.

Classe de dados: copy()

A função copy() (link em inglês) é fornecida por padrão a todas as instâncias de classes de dados. Ela é usada para copiar um objeto e mudar algumas de suas propriedades, mas mantendo o restante das propriedades inalteradas.

Por exemplo, considere a classe User e a instância jack, conforme mostrado abaixo. Caso queira criar uma nova instância atualizando somente a propriedade age, a implementação dela será assim:

Exemplo

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

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

// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
  1. Volte para a função sellItem() no InventoryViewModel. Dentro do bloco if, crie uma nova propriedade imutável chamada newItem. Chame a função copy() na instância item transmitindo o quantityInStock atualizado. Isso fará o estoque diminuir em 1.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  1. Abaixo da definição do newItem, faça uma chamada para a função updateItem() transmitindo a nova entidade atualizada, que é newItem. O método completo ficará como o exemplo a seguir.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
       // Decrease the quantity by 1
       val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
       updateItem(newItem)
   }
}
  1. Para adicionar o recurso de venda de estoque, acesse o ItemDetailFragment. Role até o fim da função bind(). No bloco apply, defina um listener de clique para o botão Sell e chame a função sellItem() no viewModel.
private fun bind(item: Item) {
binding.apply {

...
    sellItem.setOnClickListener { viewModel.sellItem(item) }
    }
}
  1. Execute o app. Na tela Inventary, clique em um elemento de lista com quantidade maior que zero. A tela Item Details será exibida. Toque no botão Sell. Observe que a quantidade de itens será reduzida em um.

aa63ca761dc8f009.png

  1. Na tela Item Details, defina a quantidade 0 tocando continuamente no botão Sell. Dica: selecione uma entidade com menos estoque ou crie uma nova entidade com uma quantidade menor. Quando a quantidade for igual a zero, toque no botão Sell. Nenhum mudança visual acontecerá. Isso ocorre porque a função sellItem() verifica se a quantidade é maior que zero antes de atualizar a quantidade.

3e099d3c55596938.png

  1. Para dar uma resposta melhor ao usuário, desative o botão Sell quando não houver nenhum item para venda. No InventoryViewModel, adicione uma função para verificar se a quantidade é maior que 0. Nomeie a função isStockAvailable(), que usa uma instância de Item e retorna um Boolean.
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. Acesse o ItemDetailFragment e role até a função bind(). Dentro do bloco "apply," chame a função isStockAvailable() no viewModel transmitindo o item. Defina o valor de retorno como a propriedade isEnabled do botão Sell. O código ficará assim:
private fun bind(item: Item) {
   binding.apply {
       ...
       sellItem.isEnabled = viewModel.isStockAvailable(item)
       sellItem.setOnClickListener { viewModel.sellItem(item) }
   }
}
  1. Execute o app. O botão Sell será desativado quando a quantidade de itens em estoque for igual a zero. Parabéns por implementar o recurso de venda de itens no app.

5e49db8451e77c2b.png

Excluir entidade do item

Como na tarefa anterior, você estenderá ainda mais os recursos do app implementando a função de exclusão. Veja abaixo as instruções para esta etapa, que é muito mais fácil do que implementar o recurso de venda.

  • Adicione uma função ao ViewModel para excluir uma entidade do banco de dados.
  • Adicione um novo método ao ItemDetailFragment para chamar a nova função de exclusão e processar a navegação.
  • Anexe um listener de clique ao botão Delete.

Vamos continuar programando:

  1. No InventoryViewModel, adicione uma nova função chamada deleteItem(), que usa uma instância da classe de entidade Item, chamada item, e não retorna nada. Na função deleteItem(), inicie uma corrotina com o viewModelScope. Dentro do bloco launch, chame o método delete() em itemDao transmitindo o item.
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. No ItemDetailFragment, role até o início da função deleteItem(). Chame deleteItem() no viewModel e transmita o item. A instância item contém a entidade exibida na tela Item Details. O método concluído ficará assim:
private fun deleteItem() {
   viewModel.deleteItem(item)
   findNavController().navigateUp()
}
  1. Ainda no ItemDetailFragment, role até a função showConfirmationDialog(). Ela é fornecida como parte do código inicial. Esse método exibe uma caixa de diálogo de alerta para pedir a confirmação do usuário antes de excluir o item e chama a função deleteItem() quando o botão positivo é tocado.
private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            ...
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

A função showConfirmationDialog() exibe uma caixa de diálogo de alerta parecida com esta:

728bfcbb997c8017.png

  1. No ItemDetailFragment no final da função bind(), dentro do bloco apply, defina o listener de clique para o botão de exclusão. Chame showConfirmationDialog() dentro do lambda do listener de clique.
private fun bind(item: Item) {
   binding.apply {
       ...
       deleteItem.setOnClickListener { showConfirmationDialog() }
   }
}
  1. Execute o app. Selecione um elemento na tela com a lista do inventário. Na tela Item Details, toque no botão Delete. Toque em "Yes" e o app retornará à tela "Inventory". A entidade excluída não estará mais no banco de dados do app. Parabéns por implementar o recurso de exclusão.

c05318ab8c216fa1.png

Editar entidade do item

Assim como nas tarefas anteriores, nesta você adicionará mais uma melhoria de recurso ao app, implementando a entidade de edição de itens.

Veja um resumo das etapas para editar uma entidade no banco de dados do app:

  • Reutilize a tela Add Item atualizando o título do fragmento para "Edit Item".
  • Adicione o listener de clique ao FAB para navegar até a tela Edit Item.
  • Preencha as TextViews com as informações da entidade.
  • Atualize a entidade no banco de dados usando o Room.

Adicionar listener de clique ao FAB

  1. No ItemDetailFragment, adicione uma nova função private chamada editItem(), que não usa parâmetros e não retorna nada. Na próxima etapa, você reutilizará o fragment_add_item.xml atualizando o título da tela para Edit Item. Para fazer isso, você enviará a string de título do fragmento junto com o ID do item como parte da ação.
private fun editItem() {
}

Depois de atualizar o título do fragmento, a tela Edit Item ficará como o exemplo a seguir.

bcd407af7c515a21.png

  1. Na função editItem(), crie uma variável imutável chamada action. Faça uma chamada para actionItemDetailFragmentToAddItemFragment() em ItemDetailFragmentDirections, transmitindo a string do título edit_fragment_title e o item id. Atribua o valor retornado para action. Abaixo da definição de action, chame this.findNavController().navigate(), transmitindo action para navegar até a tela Edit Item.
private fun editItem() {
   val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
       getString(R.string.edit_fragment_title),
       item.id
   )
   this.findNavController().navigate(action)
}
  1. Ainda no ItemDetailFragment, role até a função bind(). Dentro do bloco apply, defina o listener de clique para o FAB. Chame a função editItem() no lambda para acessar a tela Edit Item.
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. Execute o app. Navegue até a tela Item Details. Clique no FAB. O título da tela será atualizado para "Edit Item", mas todos os campos de texto ficarão vazios. Na próxima etapa, você corrigirá esse problema.

a6a6583171b68230.png

Preencher TextViews

Nesta etapa, você preencherá os campos de texto da tela Edit Item com as informações da entidade. Como estamos usando a tela Add Item, você adicionará novas funções ao arquivo Kotlin AddItemFragment.kt.

  1. No AddItemFragment, adicione uma nova função private para vincular os campos de texto às informações da entidade. Dê um nome à função bind(), que usa uma instância da classe de entidade do item e não retorna nada.
private fun bind(item: Item) {
}
  1. A implementação da função bind() é muito semelhante à implementação feita anteriormente no ItemDetailFragment. Dentro da função bind(), arredonde o preço para duas casas decimais usando a função format() e atribua-o a um val chamado price, conforme mostrado abaixo.
val price = "%.2f".format(item.itemPrice)
  1. Abaixo da definição de price, use a função de escopo apply na propriedade binding, conforme mostrado abaixo.
binding.apply {
}
  1. No bloco de código da função de escopo apply, defina item.itemName como a propriedade de texto de itemName. Use a função setText() e transmita a string item.itemName e TextView.BufferType.SPANNABLE como BufferType.
binding.apply {
   itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}

Importe android.widget.TextView, se solicitado pelo Android Studio.

  1. De forma semelhante ao que foi feito na etapa anterior, defina a propriedade de texto do preço EditText, conforme mostrado abaixo. Para definir a propriedade text da quantidade EditText, converta o item.quantityInStock em uma String. O código final ficará assim:
private fun bind(item: Item) {
   val price = "%.2f".format(item.itemPrice)
   binding.apply {
       itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
       itemPrice.setText(price, TextView.BufferType.SPANNABLE)
       itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
   }
}
  1. Ainda dentro do AddItemFragment, role até a função onViewCreated(). Após a chamada para a função de superclasse, crie um val chamado id e recupere itemId dos argumentos de navegação.
val id = navigationArgs.itemId
  1. Adicione um bloco if-else com uma condição para verificar se id é maior que zero. Mova o listener de clique do botão Save para o bloco else. Dentro do bloco if, recupere a entidade usando id e adicione um observador. No observador, atualize a propriedade item e chame bind(), transmitindo o item. A função completa é fornecida para que você possa copiar e colar. Ela é simples e fácil de entender, portanto, você poderá decifrá-la por conta própria.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   if (id > 0) {
       viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
           item = selectedItem
           bind(item)
       }
   } else {
       binding.saveAction.setOnClickListener {
           addNewItem()
       }
   }
}
  1. Execute o app, acesse Item Details e toque no FAB +. Observe que os campos serão preenchidos com os detalhes do item. Edite a quantidade em estoque ou qualquer outro campo e toque no botão "Save". Nada acontecerá. Isso ocorre porque você não está atualizando a entidade no banco de dados do app. Corrigiremos isso em breve.

829ceb9dd7993215.png

Atualizar a entidade usando o Room

Nesta tarefa final, você adicionará as partes finais do código para implementar o recurso de atualização. Você definirá as funções necessárias no ViewModel e as usará no AddItemFragment.

É hora de programar novamente.

  1. No InventoryViewModel, adicione uma função private chamada getUpdatedItemEntry(), que usa um Int e três strings para os detalhes da entidade, chamados itemName, itemPrice e itemCount. Retorne uma instância de Item da função. O código é fornecido para consulta.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
}
  1. Na função getUpdatedItemEntry(), crie uma instância de Item usando os parâmetros da função, conforme mostrado abaixo. Retorne a instância Item da função.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
   return Item(
       id = itemId,
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Ainda dentro do InventoryViewModel, adicione outra função chamada updateItem(). Essa função também usa um Int e três strings nas informações da entidade, mas não retorna nada. Use os nomes das variáveis do snippet de código a seguir.
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
}
  1. Na função updateItem(), faça uma chamada para a função getUpdatedItemEntry() transmitindo as informações da entidade, que são transmitidas como parâmetros de função, conforme mostrado abaixo. Atribua o valor retornado a uma variável imutável chamada updatedItem.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
  1. Logo abaixo da chamada para a função getUpdatedItemEntry(), faça uma chamada para a função updateItem() transmitindo o updatedItem. A função concluída fica assim:
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
   val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
   updateItem(updatedItem)
}
  1. Volte para AddItemFragment e adicione uma função privada chamada updateItem(), que não tem parâmetros e não retorna nada. Dentro da função, adicione uma condição if para validar a entrada do usuário chamando a função isEntryValid().
private fun updateItem() {
   if (isEntryValid()) {
   }
}
  1. No bloco if, faça uma chamada para viewModel.updateItem() transmitindo as informações da entidade. Use o itemId dos argumentos de navegação e as outras informações da entidade, como nome, preço e quantidade dos EditTexts, conforme mostrado abaixo.
viewModel.updateItem(
    this.navigationArgs.itemId,
    this.binding.itemName.text.toString(),
    this.binding.itemPrice.text.toString(),
    this.binding.itemCount.text.toString()
)
  1. Abaixo da chamada de função updateItem(), defina um val chamado action. Chame actionAddItemFragmentToItemListFragment() em AddItemFragmentDirections e atribua o valor retornado para action. Navegue até ItemListFragment e chame findNavController().navigate() transmitindo a action.
private fun updateItem() {
   if (isEntryValid()) {
       viewModel.updateItem(
           this.navigationArgs.itemId,
           this.binding.itemName.text.toString(),
           this.binding.itemPrice.text.toString(),
           this.binding.itemCount.text.toString()
       )
       val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
       findNavController().navigate(action)
   }
}
  1. Ainda no AddItemFragment, role até a função bind(). Dentro do bloco de funções do escopo binding.apply, defina o listener de clique do botão Save. Chame a função updateItem() dentro do lambda, conforme mostrado abaixo.
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. Execute o app. Tente editar os itens do inventário. Você conseguirá editar qualquer item do banco de dados do app Inventory.

1bbd094a77c25fc4.png

Parabéns por criar seu primeiro app usando o Room para gerenciar o banco de dados do app!

O código da solução para este codelab está no repositório do GitHub e na ramificação mostrados abaixo.

Para encontrar o código deste codelab e abri-lo no Android Studio, faça o seguinte.

Buscar o código

  1. Clique no URL fornecido. Isso abrirá a página do GitHub referente ao projeto em um navegador.
  2. Na página do GitHub do projeto, clique no botão Code, que exibirá uma caixa de diálogo.

5b0a76c50478a73f.png

  1. Na caixa de diálogo, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
  2. Localize o arquivo no computador, que provavelmente está na pasta Downloads.
  3. Clique duas vezes no arquivo ZIP para descompactá-lo. Isso criará uma nova pasta com os arquivos do projeto.

Abrir o projeto no Android Studio

  1. Inicie o Android Studio.
  2. Na janela Welcome to Android Studio, clique em Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Observação: caso o Android Studio já esteja aberto, selecione a opção File > New > Import Project.

21f3eec988dcfbe9.png

  1. Na caixa de diálogo Import Project, vá até a pasta do projeto descompactada, que provavelmente está na pasta Downloads.
  2. Clique duas vezes nessa pasta do projeto.
  3. Aguarde o Android Studio abrir o projeto.
  4. Clique no botão Run 11c34fc5e516fb1c.png para criar e executar o app. Confira se ele funciona da forma esperada.
  5. Procure os arquivos do projeto na janela de ferramentas Project para ver como o app foi implementado.
  • O Kotlin permite estender uma classe com nova função sem que seja necessário herdá-la da classe ou modificar a definição existente. Isso é feito usando declarações especiais chamadas extensões (link em inglês).
  • Para consumir os dados Flow como um valor LiveData, use a função asLiveData().
  • A função copy() (link em inglês) é fornecida por padrão a todas as instâncias de classes de dados. Ela permite que você copie um objeto e mude algumas das propriedades dele, mantendo o restante das propriedades inalteradas.

Documentação do desenvolvedor Android

Referências da API

Referências do Kotlin