Persistência de dados com o Room

A maioria dos apps de qualidade tem dados que precisam ser salvos, mesmo depois que o usuário os fecha. Por exemplo, o app pode armazenar uma playlist de músicas, itens em uma lista de tarefas, registros de despesas e renda, um catálogo de constelações ou um histórico de dados pessoais. Na maioria dos casos, um banco de dados é usado para armazenar esses dados persistentes.

O Room é uma biblioteca de persistência que faz parte do Android Jetpack. O Room é uma camada de abstração sobre um banco de dados SQLite. O SQLite usa uma linguagem especializada (SQL) para executar operações do banco de dados. Em vez de usar o SQLite diretamente, o Room simplifica as tarefas de criação, configuração e interação com o banco de dados. O Room também oferece verificações das instruções do SQLite durante o tempo de compilação.

A imagem abaixo mostra como o Room se encaixa na arquitetura geral recomendada neste curso.

7521165e051cc0d4.png

Pré-requisitos

  • Saber como criar uma interface do usuário (IU) básica para um app Android.
  • Saber como usar atividades, fragmentos e visualizações.
  • Saber como navegar entre fragmentos usando o Safe Args para transmitir dados entre eles.
  • Ter familiaridade com os Componentes de arquitetura do Android ViewModel, LiveData e Flow e saber como usar ViewModelProvider.Factory para instanciar os ViewModels.
  • Ter familiaridade com os princípios básicos de simultaneidade.
  • Saber usar corrotinas para tarefas de longa duração.
  • Ter noções básicas sobre os bancos de dados SQL e da linguagem SQLite.

O que você aprenderá

  • Como criar e interagir com o banco de dados SQLite usando a biblioteca Room.
  • Como criar uma entidade, um DAO e as classes de banco de dados.
  • Como usar um objeto de acesso a dados (DAO, na sigla em inglês) para mapear funções Kotlin para consultas SQL.

O que você criará

  • Você criará um app chamado Inventory, que salva os itens de um inventário em um banco de dados SQLite.

O que é necessário

  • Código inicial do app Inventory.
  • Um computador com o Android Studio instalado.

Neste codelab, você trabalhará com um app inicial chamado Inventory e adicionará nele a camada do banco de dados usando a biblioteca Room. A versão final do app exibirá uma lista de itens do banco de dados de inventário usando uma RecyclerView. O usuário terá as opções de adicionar um novo item, atualizar um existente e excluir um item do banco de dados do inventário. Você completará as funções do app no próximo codelab.

Veja abaixo algumas capturas de tela da versão final do app.

52087556378ea8db.png

Fazer o download do código inicial para este codelab

Este codelab oferece um código inicial para você estender com os recursos ensinados. O código inicial pode conter um código que você já conheceu em codelabs anteriores e também trechos novos que serão apresentados nos próximos codelabs.

Se você usar o código inicial do GitHub, o nome da pasta é android-basics-kotlin-inventory-app-starter. Selecione essa pasta ao abrir o projeto no Android Studio.

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.

Visão geral do código inicial

  1. Abra o projeto com o código inicial no Android Studio.
  2. Execute o app em um dispositivo Android ou em um emulador. Verifique se o emulador ou dispositivo conectado está executando a API de nível 26 ou mais recente. O Database Inspector funciona melhor em emuladores/dispositivos com API de nível 26.
  3. O app não exibirá dados de inventário. Há um FAB na tela para adicionar novos itens ao banco de dados.
  4. Clique no FAB. O app abrirá uma nova tela em que é possível inserir as informações do novo item.

9c5e361a89453821.png

Problemas com o código inicial

  1. Na tela Add item, insira os detalhes do item. Toque em Save. O fragmento de adição de item fechará, e o usuário será levado de volta ao fragmento anterior. O novo item não ficará salvo nem listado na tela do inventário. O app ainda não está completo, e a função do botão Save não está implementada.

f0931dab5089a14f.png

Neste codelab, você adicionará a parte do banco de dados de um app que salva as informações de inventário no banco de dados SQLite. Você usará a biblioteca de persistência Room para interagir com o banco de dados SQLite.

Tutorial do código

O código inicial que foi transferido por download tem os layouts de tela pré-criados para você. Nesse módulo, você se concentrará em implementar a lógica do banco de dados. Veja a seguir um breve tutorial sobre alguns dos arquivos para começar.

main_activity.xml

É a atividade principal que hospeda todos os outros fragmentos no app. O método onCreate() recupera o NavController do NavHostFragment e configura a barra de ação para uso com o NavController.

item_list_fragment.xml

É a primeira tela exibida no app, que contém principalmente uma RecyclerView e um FAB. Você implementará a RecyclerView nos próximos módulos.

fragment_add_item.xml

Esse layout contém campos de texto para inserir as informações do novo item de inventário a ser adicionado.

ItemListFragment.kt

Esse fragmento contém principalmente o código boilerplate. No método onViewCreated(), o listener de clique é definido no FAB para navegar até o fragmento de adição de itens.

AddItemFragment.kt

Esse fragmento é usado para adicionar novos itens ao banco de dados. A função onCreateView() inicializa a variável de vinculação e a função onDestroyView() oculta o teclado antes de destruir o fragmento.

O Kotlin oferece uma forma simples de processar dados, introduzindo as classes de dados. Esses dados são acessados e possivelmente modificados usando chamadas de função. No entanto, no mundo dos bancos de dados, é necessário ter tabelas e consultas para acessar e modificar dados. Os componentes do Room apresentados a seguir simplificam esses fluxos de trabalho.

Existem três componentes principais no Room:

  • As entidades de dados representam tabelas no banco de dados do app. Elas são usados para atualizar os dados armazenados em linhas nas tabelas e para criar novas linhas para inserção.
  • Os objetos de acesso a dados (DAOs, na sigla em inglês) fornecem métodos usados pelo app para recuperar, atualizar, inserir e excluir dados no banco de dados.
  • A classe de banco de dados hospeda o banco de dados e é o principal ponto de acesso para a conexão com o banco de dados do app. A classe do banco de dados fornece ao seu app instâncias dos DAOs associadas ao banco de dados.

Você implementará esses componentes e aprenderá mais sobre eles posteriormente no codelab. O diagrama a seguir demonstra como os componentes do Room trabalham em conjunto para interagir com o banco de dados.

33a193a68c9a8e0e.png

Adicionar bibliotecas Room

Nesta tarefa, você adicionará as bibliotecas de componentes Room necessárias aos arquivos do Gradle.

  1. Abra o arquivo do Gradle build.gradle (Module: InventoryApp.app) no módulo. No bloco dependencies, adicione as seguintes dependências à biblioteca Room.
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

A classe Entity define uma tabela. Cada instância dessa classe representa uma linha na tabela do banco de dados. A classe Entity tem mapeamentos para informar ao Room como ela pretende apresentar e interagir com as informações no banco de dados. No app, a entidade manterá informações sobre os itens do inventário, como nome, preço e estoque disponível.

8c9f1659ee82ca43.png

A anotação @Entity marca uma classe como Entity do banco de dados. Uma tabela de banco de dados é criada para armazenar os itens de cada classe Entity. Cada campo da classe é representado como uma coluna no banco de dados, a menos que seja indicado de outra forma. Consulte os documentos da Entity para ver mais informações. Todas as instâncias de entidade armazenadas no banco de dados precisam ter uma chave primária. A chave primária é usada para identificar de forma exclusiva cada registro/entrada das tabelas do banco de dados. Uma vez atribuída, a chave primária não pode ser modificada. Ela representará o objeto da entidade enquanto ele existir no banco de dados.

Nesta tarefa, você criará uma classe Entity. Defina os campos para armazenar as informações a seguir sobre cada item do inventário.

  • Um Int para armazenar a chave primária.
  • Uma String para armazenar o nome do item.
  • Um double para armazenar o preço do item.
  • Um Int para armazenar a quantidade em estoque.
  1. Abra o código inicial no Android Studio.
  2. Crie um pacote chamado data no pacote de base com.example.inventory.

be39b42484ba2664.png

  1. No pacote data, crie uma classe do Kotlin com o nome Item. Essa classe representará uma entidade do banco de dados no seu app. Na próxima etapa, você adicionará campos correspondentes para armazenar informações de inventário.
  2. Atualize a definição da classe Item com o código a seguir. Declare id do tipo Int, itemName do tipo String,, itemPrice do tipo Double e quantityInStock do tipo Int como parâmetros para o construtor principal. Atribua um valor padrão de 0 ao id. Essa será a chave primária, um ID para identificar de forma exclusiva cada registro/entrada da tabela Item.
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)

Classes de dados

As classes de dados são usadas principalmente para armazenar dados no Kotlin. Elas são marcadas com a palavra-chave data. Os objetos de classe de dados do Kotlin têm alguns outros benefícios. O compilador gera utilitários automaticamente para comparar, imprimir e copiar, como toString(), copy() (link em inglês) e equals().

Exemplo:

// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}

Para garantir a consistência e o comportamento significativo do código gerado, as classes de dados precisam atender aos seguintes requisitos:

  • O construtor principal precisa ter pelo menos um parâmetro.
  • Todos os parâmetros do construtor principal precisam ser marcados como val ou var.
  • As classes de dados não podem ser abstract, open, sealed ou inner.

Para saber mais sobre as classes de dados, consulte a documentação (link em inglês).

  1. Converta a classe Item em uma classe de dados, adicionando a palavra-chave data como prefixo à definição da classe.
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Acima da declaração de classe Item, faça a anotação @Entity na classe de dados. Use o argumento tableName para usar o item como nome da tabela SQLite.
@Entity(tableName = "item")
data class Item(
   ...
)
  1. Para identificar o id como a chave primária, faça uma anotação com @PrimaryKey na propriedade id. Defina o parâmetro autoGenerate como true, para que o Room gere um ID para cada entidade. Isso garante que o ID de cada item será exclusivo.
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. Faça uma anotação com @ColumnInfo nas propriedades restantes. A anotação ColumnInfo é usada para personalizar a coluna associada ao campo específico. Por exemplo, ao usar o argumento name, é possível especificar um nome de coluna diferente para o campo, em vez do nome da variável. Personalize os nomes de propriedade usando parâmetros, conforme mostrado abaixo. Essa abordagem é semelhante a usar tableName para especificar um nome diferente para o banco de dados.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

Objeto de acesso a dados (DAO)

O objeto de acesso a dados (DAO, na sigla em inglês) é um padrão usado para separar a camada de persistência do restante do app, fornecendo uma interface abstrata. Esse isolamento segue o princípio de responsabilidade única (link em inglês), abordado nos codelabs anteriores.

A função do DAO é ocultar do restante do aplicativo todas as complexidades envolvidas na execução das operações do banco de dados na camada de persistência subjacente. Isso permite fazer mudanças na camada de acesso aos dados de forma independente do código que usa os dados.

dcef2fc739d704e5.png

Nesta tarefa, você definirá um objeto de acesso a dados (DAO, na sigla em inglês) para o Room. Os objetos de acesso a dados são os principais componentes do Room responsáveis por definir a interface que acessa o banco de dados.

O DAO que você criará será uma interface personalizada que fornece métodos convenientes para consultar/recuperar, inserir, excluir e atualizar o banco de dados. O Room gerará uma implementação dessa classe no momento da compilação.

Para operações comuns do banco de dados, a biblioteca Room fornece anotações de conveniência, como @Insert, @Delete e @Update. A anotação @Query é usado para todo o restante. É possível programar qualquer consulta compatível com o SQLite.

Outro benefício é que à medida que você programa suas consultas no Android Studio, o compilador verifica se há erros de sintaxe nas consultas SQL.

Para o app Inventory, você precisa fazer o seguinte:

  • Inserir ou adicionar um novo item.
  • Atualizar um item existente para mudar o nome, o preço e a quantidade.
  • Buscar um item específico com base na chave primária id.
  • Buscar todos os itens para poder exibi-los.
  • Excluir uma entrada do banco de dados.

bb381857d5fba511.png

Agora, implemente o DAO do item no app:

  1. No pacote data, crie a classe ItemDao.kt do Kotlin.
  2. Mude a definição da classe para interface e inclua uma anotação @Dao.
@Dao
interface ItemDao {
}
  1. Dentro do corpo da interface, adicione uma anotação @Insert. Abaixo de @Insert, adicione uma função insert() que aceita uma instância do item da classe Entity como argumento. Como as operações do banco de dados podem levar muito tempo para ser executadas, isso precisa ser feito em uma linha de execução separada. Transforme a função em uma função de suspensão para que ela seja chamada em uma corrotina.
@Insert
suspend fun insert(item: Item)
  1. Adicione um argumento OnConflict e atribua a ele um valor de OnConflictStrategy.IGNORE. O argumento OnConflict instrui o Room sobre o que fazer em caso de conflito. A estratégia OnConflictStrategy.IGNORE ignora um novo item se a chave primária já estiver no banco de dados. Para saber mais sobre as estratégias de conflito disponíveis, confira a documentação.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Agora, o Room gerará todo o código necessário para inserir o item no banco de dados. Quando você chama insert() no código do Kotlin, o Room executa uma consulta SQL para inserir a entidade no banco de dados. Observação: a função pode ter o nome que você quiser. Ela não precisa ser chamada de insert().

  1. Adicione uma anotação @Update com uma função update() a um item. A entidade atualizada tem a mesma chave que a entidade transmitida. É possível atualizar algumas ou todas as outras propriedades da entidade. De modo semelhante ao que foi feito no método insert(), faça o método update() seguinte ser suspend.
@Update
suspend fun update(item: Item)
  1. Adicione a anotação @Delete com uma função delete() para excluir itens. Transforme-a em um método de suspensão. A anotação @Delete exclui um item ou uma lista de itens. Observação: é preciso transmitir as entidades que serão excluídas. Se você não tiver a entidade, precisará buscá-la antes de chamar a função delete().
@Delete
suspend fun delete(item: Item)

Não existe uma anotação de conveniência para as funções restantes. Portanto, é necessário usar a anotação @Query e oferecer consultas SQLite.

  1. Programe uma consulta SQLite para recuperar um item específico da tabela de itens com base no id fornecido. Em seguida, adicione a anotação Room e use uma versão modificada da consulta a seguir nas etapas posteriores. Nas próximas etapas, você também mudará isso para um método DAO usando o Room.
  2. Selecione todas as colunas do item.
  3. WHERE (onde) o id corresponde a um valor específico.

Exemplo:

SELECT * from item WHERE id = 1
  1. Mude a consulta SQL acima para usá-la com a anotação do Room e com um argumento. Adicione uma anotação @Query e forneça a consulta como um parâmetro de string à anotação @Query. Adicione um parâmetro String à @Query, que é uma consulta SQLite, para recuperar um item da tabela de itens.
  2. Selecione todas as colunas do item.
  3. WHERE (onde) o id corresponde ao argumento :id. Observe o :id. Os dois pontos são usados na consulta para referenciar argumentos na função.
@Query("SELECT * from item WHERE id = :id")
  1. Abaixo da anotação @Query, insira a função getItem() que usa um argumento Int e retorna um Flow<Item>.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Usar o Flow ou o LiveData como tipo de retorno garantirá que uma notificação seja enviada sempre que os dados no banco de dados mudarem. É recomendável usar o Flow na camada de persistência. O Room mantém esse Flow atualizado para você, o que significa que você só precisa receber os dados explicitamente uma vez. Isso é útil para atualizar a lista de inventário, que será implementada no próximo codelab. Devido ao tipo de retorno Flow, o Room também executa a consulta na linha de execução em segundo plano. Não é necessário torná-la uma função suspend explicitamente e chamá-la dentro de um escopo de corrotina.

Pode ser necessário importar o Flow de kotlinx.coroutines.flow.Flow.

  1. Adicione uma @Query com uma função getItems().
  2. Faça com que a consulta SQLite retorne todas as colunas da tabela item, em ordem crescente.
  3. Faça com que getItems() retorne uma lista de entidades Item como um Flow. O Room mantém esse Flow atualizado para você, o que significa que você só precisa receber os dados explicitamente uma vez.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. Não será possível visualizar nenhuma mudança, mas execute o app mesmo assim para garantir que não haja erros.

Nesta tarefa, você criará um RoomDatabase que usa a Entity e o DAO criados na tarefa anterior. A classe do banco de dados define a lista de entidades e objetos de acesso a dados. Ela também é o principal ponto de acesso da conexão.

A classe Database fornece instâncias dos DAOs definidos ao app. O app pode usar os DAOs para recuperar dados do banco de dados como instâncias dos objetos da entidade de dados associados. Ele também pode usar as entidades de dados definidas para atualizar linhas das tabelas correspondentes ou criar novas linhas para inserção.

É necessário criar uma classe RoomDatabase abstrata, com a anotação @Database. Essa classe tem um método que cria uma instância do RoomDatabase, caso não exista uma, ou retorna a instância atual do RoomDatabase.

Veja a seguir o processo geral para retornar a instância do RoomDatabase:

  • Crie uma classe public abstract que estenda o RoomDatabase. A nova classe abstrata definida hospeda o banco de dados. A classe definida é abstrata, porque o Room criará a implementação para você.
  • Adicione a anotação @Database à classe. Nos argumentos, liste as entidades do banco de dados e configure o número da versão.
  • Defina um método ou uma propriedade abstrata que retorne uma instância ItemDao. O Room gerará a implementação para você.
  • Só é necessária uma instância do RoomDatabase para todo o app. Portanto, transforme o RoomDatabase em um singleton.
  • Use o Room.databaseBuilder do Room para criar seu banco de dados (item_database) somente se ele não existir. Caso contrário, retorne o banco de dados existente.

Criar o banco de dados

  1. No pacote data, crie uma classe do Kotlin com o nome ItemRoomDatabase.kt.
  2. No arquivo ItemRoomDatabase.kt, transforme o ItemRoomDatabase em uma classe abstract que estenda o RoomDatabase. Adicione a anotação @Database à classe. O erro que indica a ausência de parâmetros será corrigido na próxima etapa.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. A anotação @Database requer vários argumentos para que o Room possa criar o banco de dados.
  • Especifique o Item como a única classe com a lista de entities.
  • Defina a version como 1. Sempre que o esquema da tabela do banco de dados mudar, será necessário aumentar o número da versão.
  • Defina o exportSchema como false, para não manter os backups do histórico de versões do esquema.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. O banco de dados precisa ser informado sobre o DAO. No corpo da classe, declare uma função abstrata que retorne o ItemDao. É possível ter vários DAOs.
abstract fun itemDao(): ItemDao
  1. Abaixo da função abstrata, defina um objeto companion. O objeto complementar permite acessar os métodos para criar ou consultar o banco de dados, usando o nome da classe como qualificador.
 companion object {}
  1. Dentro do objeto companion, declare uma variável privada anulável INSTANCE para o banco de dados e inicialize-a como null. A variável INSTANCE manterá uma referência ao banco de dados quando uma conta for criada. Isso ajuda a manter uma única instância do banco de dados aberta em determinado momento, porque ela é um recurso com criação e manutenção caras.

Inclua a anotação @Volatile na INSTANCE. O valor de uma variável volátil nunca será armazenado em cache. Todas as gravações e leituras serão feitas da memória principal. Isso ajuda a garantir que o valor da INSTANCE esteja sempre atualizado e seja o mesmo em todas as linhas de execução. Isso significa que as mudanças feitas por uma linha de execução na INSTANCE ficam visíveis para todas as outras linhas imediatamente.

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. Abaixo da INSTANCE, ainda dentro do objeto companion, defina um método getDatabase() com um parâmetro Context, necessário para o builder do banco de dados. Retorne um tipo ItemRoomDatabase. Você verá um erro, porque getDatabase() ainda não retorna nada.
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. Várias linhas de execução podem entrar em disputa e solicitar uma instância do banco de dados ao mesmo tempo, resultando em dois bancos de dados, em vez de um. Envolver o código para receber o banco de dados em um bloco synchronized significa que somente uma linha de execução poderá entrar nesse bloco de código por vez, garantindo que o banco de dados será inicializado apenas uma vez.

Dentro de getDatabase(), retorne a variável INSTANCE ou, caso a INSTANCE seja nula, inicialize-a dentro de um bloco synchronized{}. Use o operador Elvis (?:) para fazer isso. Transmita o objeto complementar this, que será bloqueado dentro do bloco da função. O erro será corrigido nas próximas etapas.

return INSTANCE ?: synchronized(this) { }
  1. Dentro do bloco sincronizado, crie uma variável de instância val e use o builder do banco de dados para retornar o banco de dados. Você ainda verá erros, mas eles serão corrigidos nas próximas etapas.
val instance = Room.databaseBuilder()
  1. No final do bloco synchronized, retorne uma instance.
return instance
  1. Dentro do bloco synchronized, inicialize a variável instance e use o builder do banco de dados para retornar um banco de dados. Transmita o contexto do aplicativo, a classe do banco de dados e um nome para o banco de dados, item_database para Room.databaseBuilder().
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

O Android Studio gerará um erro de correspondência de tipo. Para remover esse erro, será necessário adicionar uma estratégia de migração e build() nas etapas a seguir.

  1. Adicione a estratégia de migração necessária ao builder. Use .fallbackToDestructiveMigration().

Normalmente, é necessário fornecer um objeto de migração com uma estratégia de migração para os momentos em que o esquema mudar. Um objeto de migração é aquele que define como converter todas as linhas no esquema antigo em linhas no novo esquema, para que nenhum dado seja perdido. A migração (link em inglês) está além do escopo deste codelab. Uma solução simples é destruir e recriar o banco de dados, o que significa que os dados serão perdidos.

.fallbackToDestructiveMigration()
  1. Para criar a instância do banco de dados, chame .build(). Isso removerá os erros do Android Studio.
.build()
  1. Dentro do bloco synchronized, atribua INSTANCE = instance.
INSTANCE = instance
  1. No final do bloco synchronized, retorne uma instance. O código concluído ficará assim:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

   abstract fun itemDao(): ItemDao

   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null
       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  1. Compile seu código para garantir que não haja erros.

Implementar a classe Application

Nesta tarefa, você criará a instância do banco de dados na classe Application.

  1. Abra InventoryApplication.kt, crie um val chamado database do tipo ItemRoomDatabase. Instancie a instância database chamando getDatabase() em ItemRoomDatabase, transmitindo o contexto. Use o delegado lazy para que a instância database seja criada lentamente quando você precisar da referência ou acessá-la pela primeira vez, e não quando o app for iniciado. Isso criará o banco de dados físico no disco no primeiro acesso.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase

class InventoryApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

Você usará essa instância do database posteriormente no codelab ao criar uma instância do ViewModel.

Agora você tem os elementos básicos para trabalhar com o Room. É possível compilar e executar esse código, mas ainda não dá para saber se ele realmente funciona. Portanto, este é um bom momento para adicionar um novo item ao banco de dados do app Inventory para testá-lo. Para fazer isso, você precisa de um ViewModel para se comunicar com o banco de dados.

Até agora, você criou um banco de dados com as classes de IU como parte do código inicial. Para salvar os dados temporários do app e acessar o banco de dados, você precisa de um ViewModel. O ViewModel do app Inventory interagirá com o banco de dados usando o DAO e fornecerá os dados para a IU. Todas as operações do banco de dados precisarão ser executadas na linha de execução de IU principal. Isso será feito com corrotinas e o viewModelScope.

91298a7c05e4f5e0.png

Criar ViewModel do app Inventory

  1. No pacote com.example.inventory, crie uma classe do Kotlin com o nome InventoryViewModel.kt.
  2. Estenda a classe InventoryViewModel da ViewModel. Transmita o objeto ItemDao como um parâmetro para o construtor padrão.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. No final do arquivo InventoryViewModel.kt fora da classe, adicione a classe InventoryViewModelFactory para criar a instância InventoryViewModel. Transmita o mesmo parâmetro do construtor do InventoryViewModel, que é a instância do ItemDao. Estenda a classe da ViewModelProvider.Factory. O erro relacionado aos métodos não implementados será corrigido na próxima etapa.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. Clique na lâmpada vermelha e selecione Implement Members. Também é possível substituir o método create() na classe ViewModelProvider.Factory da forma apresentada a seguir, que aceita qualquer tipo de classe como argumento e retorna um objeto ViewModel.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. Implemente o método create(). Verifique se a modelClass é igual à da classe InventoryViewModel e retorne uma instância dela. Caso contrário, gere uma exceção.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")

Preencher o ViewModel

Nesta tarefa, você preencherá a classe InventoryViewModel para adicionar dados de inventário ao banco de dados. Observe a entidade Item e a tela Adicionar item no app Inventory.

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

85c644aced4198c5.png

Para adicionar uma entidade ao banco de dados, é necessário saber o nome, o preço e a disponibilidade em estoque do item. Mais adiante no codelab, você usará a tela Add Item para receber essas informações do usuário. Nesta tarefa, você usará três strings como entrada para o ViewModel, as converterá em uma instância da entidade Item e salvará o resultado no banco de dados usando a instância ItemDao. É hora de implementar isso.

  1. Na classe InventoryViewModel, adicione uma função private chamada insertItem(), que usa um objeto Item e adiciona os dados ao banco de dados sem gerar bloqueios.
private fun insertItem(item: Item) {
}
  1. Para interagir com o banco de dados fora da linha de execução principal, inicie uma corrotina e chame o método DAO dentro dela. No método insertItem(), use viewModelScope.launch para iniciar uma corrotina no ViewModelScope. Dentro da função de inicialização, chame a função de suspensão insert() no itemDao, transmitindo o item. O ViewModelScope é uma propriedade de extensão para a classe ViewModel que cancela automaticamente as corrotinas filhas quando o ViewModel é destruído.
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

Importe kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item, se isso não for feito automaticamente.

  1. Na classe InventoryViewModel, adicione outra função particular que use três strings e retorne uma instância Item.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Ainda na classe InventoryViewModel, adicione uma função pública chamada addNewItem(), que aceita três strings de detalhes do item. Transmita as strings de detalhes do item para a função getNewItemEntry() e atribua o valor retornado a um valor chamado newItem. Faça uma chamada para insertItem() transmitindo o insertItem para adicionar a nova entidade ao banco de dados. Isso será chamado no fragmento da IU para adicionar detalhes do item ao banco de dados.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}

Você não usou o viewModelScope.launch para addNewItem(), mas ele é necessário no caso acima, em insertItem(), ao chamar um método DAO. Isso ocorre porque as funções de suspensão só podem ser chamadas em uma corrotina ou em outra função de suspensão. A função itemDao.insert(item) é de suspensão.

Você adicionou todas as funções necessárias para acrescentar entidades ao banco de dados. Na próxima tarefa, você atualizará o fragmento Add Item para usar as funções acima.

  1. Em AddItemFragment.kt, no início da classe AddItemFragment, crie um private val chamado viewModel, do tipo InventoryViewModel. Use o delegado de propriedade by activityViewModels() do Kotlin para compartilhar o ViewModel entre os fragmentos Você corrigirá o erro na próxima etapa.
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. Dentro do lambda, chame o construtor InventoryViewModelFactory() e transmita a instância ItemDao. Use a instância database, criada em uma das tarefas anteriores, para chamar o construtor itemDao.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. Abaixo da definição do viewModel, crie um lateinit var chamado item, do tipo Item.
 lateinit var item: Item
  1. A tela Add Item contém três campos de texto para que o usuário possa inserir os detalhes do item. Nesta etapa, você adicionará uma função para verificar se o texto em TextFields não está vazio. Essa função será usada para verificar a entrada do usuário antes de adicionar ou atualizar a entidade no banco de dados. Essa validação precisa ser feita no ViewModel, e não no fragmento. Na classe InventoryViewModel, adicione a seguinte função public chamada isEntryValid().
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. Em AddItemFragment.kt, abaixo da função onCreateView(), crie uma função private, chamada isEntryValid(), que retorna um Boolean. O erro que indica a ausência de valor de retorno será corrigido na próxima etapa.
private fun isEntryValid(): Boolean {
}
  1. Implemente a função isEntryValid() na classe AddItemFragment. Chame a função isEntryValid() na instância viewModel, transmitindo o texto das visualizações de texto. Retorne o valor da função viewModel.isEntryValid().
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. Na classe AddItemFragment, abaixo da função isEntryValid(), adicione outra função private chamada addNewItem(), que não tem parâmetros e não retorna nada. Dentro da função, chame isEntryValid() na condição if.
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. No bloco if, chame o método addNewItem() na instância viewModel. Transmita os detalhes do item inseridos pelo usuário e use a instância binding para fazer a leitura deles.
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. Abaixo do bloco if, crie um val action para voltar ao ItemListFragment. Chame findNavController().navigate() transmitindo a action.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

Importe androidx.navigation.fragment.findNavController..

  1. O método completo ficará assim:
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
   }
}
  1. Para juntar tudo, adicione um gerenciador de cliques ao botão Save. Na classe AddItemFragment, acima da função onDestroyView(), modifique a função onViewCreated().
  2. Dentro da função onViewCreated(), adicione um gerenciador de cliques ao botão "Save" e chame addNewItem().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. Compile e execute o app. Toque no FAB +. Na tela Add Item, insira os detalhes do item e toque em Save. Essa ação salva os dados, mas ainda não será possível visualizar nada no app. Na próxima tarefa, você usará o Database Inspector para visualizar os dados salvos.

193c7fa9c41e0819.png

Visualizar o banco de dados usando o Database Inspector

  1. Execute o app em um emulador ou dispositivo conectado com a API de nível 26 ou mais recente, caso ainda não tenha feito isso. O Database Inspector funciona melhor em emuladores/dispositivos com API de nível 26.
  2. No Android Studio, selecione View > Tool Windows > Database Inspector na barra de menus.
  3. No painel do Database Inspector, selecione com.example.inventory no menu suspenso.
  4. O item_database do app Inventory será exibido no painel Databases. Expanda o nó de item_database e selecione Item para realizar a inspeção. Se o painel Databases estiver vazio, use o emulador para adicionar alguns itens ao banco de dados usando a tela Add Item.
  5. Marque a caixa de seleção Live updates no Database Inspector para atualizar automaticamente os dados apresentados à medida que você interage com o app em execução no emulador ou dispositivo.

4803c08f94e34118.png

Parabéns! Você criou um app que consegue persistir dados usando o Room. No próximo codelab, você adicionará uma RecyclerView ao app para exibir os itens do banco de dados e adicionará novos recursos, como exclusão e atualização de entidades. Até lá!

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.
  • Defina suas tabelas como classes de dados com a anotação @Entity. Defina as propriedades com a anotação @ColumnInfo como colunas em tabelas.
  • Defina um objeto de acesso a dados (DAO, na sigla em inglês) como uma interface com a anotação @Dao. O DAO mapeia funções do Kotlin para realizar consultas no banco de dados.
  • Use anotações para definir as funções @Insert, @Delete e @Update.
  • Use a anotação @Query com uma string de consulta do SQLite como parâmetro para outras consultas.
  • Use o Database Inspector para visualizar os dados salvos no banco de dados SQLite do Android.

Documentação do desenvolvedor Android

Postagens do blog

Vídeos

Outros documentos e artigos