1. Antes de começar
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.
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 da arquitetura do Android
ViewModel
,LiveData
eFlow
e saber como usarViewModelProvider.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ê vai 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.
2. Visão geral do app
Neste codelab, você vai 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ê vai completar as funções do app no próximo codelab.
Veja abaixo algumas capturas de tela da versão final do app.
3. Visão geral do app inicial
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 abrir no Android Studio, faça o seguinte.
Acessar o código
- Clique no URL fornecido. Isso abre a página do GitHub referente ao projeto em um navegador.
- Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma caixa de diálogo.
- Na caixa de diálogo, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
- Localize o arquivo no computador, que provavelmente está na pasta Downloads.
- Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.
Abrir o projeto no Android Studio
- Inicie o Android Studio.
- Na janela Welcome to Android Studio, clique em Open an existing Android Studio project.
Observação: caso o Android Studio já esteja aberto, selecione a opção File > New > Import Project.
- Na caixa de diálogo Import Project, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
- Clique duas vezes nessa pasta do projeto.
- Aguarde o Android Studio abrir o projeto.
- Clique no botão Run
para criar e executar o app. Confira se ele é compilado da forma esperada.
- Procure os arquivos do projeto na janela de ferramentas Project para verificar como o app está configurado.
Visão geral do código inicial
- Abra o projeto com o código inicial no Android Studio.
- 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.
- O app não exibirá dados de inventário. Há um FAB na tela para adicionar novos itens ao banco de dados.
- Clique no FAB. O app abrirá uma nova tela em que é possível inserir as informações do novo item.
Problemas com o código inicial
- Na tela Add item, insira os detalhes do item. Toque em Save. O fragmento de adição de item não vai ser fechado. Navegue de volta usando a tecla "Voltar" do sistema. O novo item não vai ser 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 foi implementada.
Neste codelab, você vai 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 do 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, o foco será 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 a classe NavController
da NavHostFragment
e configura a barra de ação para uso com a 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.
4. Principais componentes do Room
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 extrair, 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ê vai implementar esses componentes e aprender mais sobre eles mais adiante no codelab. O diagrama a seguir demonstra como os componentes do Room trabalham em conjunto para interagir com o banco de dados.
Adicionar bibliotecas Room
Nesta tarefa, você vai adicionar as bibliotecas de componentes Room necessárias aos arquivos do Gradle.
- Abra o arquivo do Gradle
build.gradle (Module: InventoryApp.app)
no módulo. No blocodependencies
, 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"
5. Criar a Entity de um item
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.
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ê vai 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.
- Abra o código inicial no Android Studio.
- Crie um pacote chamado
data
no pacote de basecom.example.inventory
.
- No pacote
data
, crie uma classe do Kotlin com o nomeItem
. 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. - Atualize a definição da classe
Item
com o código a seguir. Declareid
do tipoInt
,itemName
do tipoString,
,itemPrice
do tipoDouble
equantityInStock
do tipoInt
como parâmetros para o construtor principal. Atribua um valor padrão de0
aoid
. Essa será a chave primária, um ID para identificar de forma exclusiva cada registro/entrada da tabela deItem
.
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
ouvar
. - As classes de dados não podem ser
abstract
,open
,sealed
ouinner
.
Para saber mais sobre as classes de dados, consulte a documentação (link em inglês).
- Converta a classe
Item
em uma classe de dados, adicionando a palavra-chavedata
como prefixo à definição da classe.
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
- Acima da declaração de classe
Item
, faça a anotação@Entity
na classe de dados. Use o argumentotableName
para usar oitem
como nome da tabela SQLite.
@Entity(tableName = "item")
data class Item(
...
)
- Para identificar o
id
como a chave primária, faça uma anotação na propriedadeid
com@PrimaryKey
. Defina o parâmetroautoGenerate
comotrue
, para que oRoom
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,
...
)
- Faça uma anotação com
@ColumnInfo
nas propriedades restantes. A anotaçãoColumnInfo
é usada para personalizar a coluna associada ao campo específico. Por exemplo, ao usar o argumentoname
, é 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 usartableName
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
)
6. Criar o DAO do item
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.
Nesta tarefa, você vai definir um objeto de acesso a dados (DAO) 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ê vai 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 durante a 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 com suporte do 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.
Agora, implemente o DAO do item no app:
- No pacote
data
, crie a classeItemDao.kt
do Kotlin. - Mude a definição da classe para
interface
e inclua uma anotação@Dao
.
@Dao
interface ItemDao {
}
- Dentro do corpo da interface, adicione uma anotação
@Insert
. Abaixo de@Insert
, adicione uma funçãoinsert()
que aceita uma instância doitem
da classeEntity
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)
- Adicione um argumento
OnConflict
e atribua a ele um valor deOnConflictStrategy.
IGNORE
. O argumentoOnConflict
instrui o Room sobre o que fazer em caso de conflito. A estratégiaOnConflictStrategy.
IGNORE
ignorará 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 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()
.
- Adicione uma anotação
@Update
com uma funçãoupdate()
a umitem
. 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étodoinsert()
, faça o métodoupdate()
seguinte sersuspend
(suspenso).
@Update
suspend fun update(item: Item)
- Adicione a anotação
@Delete
com uma funçãodelete()
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çãodelete()
.
@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.
- 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. - Selecione todas as colunas do
item
. WHERE
(onde) oid
corresponde a um valor específico.
Exemplo:
SELECT * from item WHERE id = 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âmetroString
à@Query
, que é uma consulta SQLite, para recuperar um item da tabela. - Selecione todas as colunas do
item
. WHERE
(onde) oid
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")
- Abaixo da anotação
@Query
, insira a funçãogetItem()
que usa um argumentoInt
e retorna umFlow<Item>
.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
Usar o Flow
ou o LiveData
como tipo de retorno vai 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 acessar 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
.
- Adicione uma
@Query
com uma funçãogetItems()
. - Faça com que a consulta SQLite retorne todas as colunas da tabela
item
, em ordem crescente. - Faça com que
getItems()
retorne uma lista de entidadesItem
como umFlow
. ORoom
mantém esseFlow
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>>
- Não será possível visualizar nenhuma mudança, mas execute o app mesmo assim para garantir que não haja erros.
7. Criar uma instância de banco de dados
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 da RoomDatabase
, caso não exista uma, ou retorna a instância atual da RoomDatabase
.
Veja a seguir o processo geral para retornar a instância do RoomDatabase
:
- Crie uma classe
public abstract
que estenda oRoomDatabase
. A nova classe abstrata definida hospeda o banco de dados. A classe definida é abstrata, porque oRoom
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
. ORoom
vai gerar a implementação para você. - Só é necessária uma instância da
RoomDatabase
para todo o app. Portanto, transforme aRoomDatabase
em um Singleton. - Use o
Room.databaseBuilder
doRoom
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
- No pacote
data
, crie uma classe do Kotlin com o nomeItemRoomDatabase.kt
. - No arquivo
ItemRoomDatabase.kt
, transforme oItemRoomDatabase
em uma classeabstract
que estenda oRoomDatabase
. 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() {}
- A anotação
@Database
requer vários argumentos para que oRoom
possa criar o banco de dados.
- Especifique o
Item
como a única classe com a lista deentities
. - Defina a
version
como1
. Sempre que o esquema da tabela do banco de dados mudar, será necessário aumentar o número da versão. - Defina o
exportSchema
comofalse
, para não manter os backups do histórico de versões do esquema.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 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
- 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 {}
- Dentro do objeto
companion
, declare uma variável particular anulávelINSTANCE
para o banco de dados e a inicialize comonull
. A variávelINSTANCE
manterá uma referência ao banco de dados quando uma tiver sido 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
- Abaixo da
INSTANCE
, ainda dentro do objetocompanion
, defina um métodogetDatabase()
com um parâmetroContext
, necessário para o builder do banco de dados. Retorne um tipoItemRoomDatabase
. Você verá um erro, porquegetDatabase()
ainda não retorna nada.
fun getDatabase(context: Context): ItemRoomDatabase {}
- 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 vai 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) { }
- 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()
- No final do bloco
synchronized
, retorne umainstance
.
return instance
- Dentro do bloco
synchronized
, inicialize a variávelinstance
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
paraRoom.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.
- 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()
- Para criar a instância do banco de dados, chame
.build()
. Isso removerá os erros do Android Studio.
.build()
- Dentro do bloco
synchronized
, atribuaINSTANCE = instance
.
INSTANCE = instance
- No final do bloco
synchronized
, retorne umainstance
. 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
}
}
}
}
- Compile seu código para garantir que não haja erros.
Implementar a classe Application
Nesta tarefa, você vai criar a instância do banco de dados na classe Application.
- Abra
InventoryApplication.kt
, crie umval
chamadodatabase
do tipoItemRoomDatabase
. ChamegetDatabase()
na classeItemRoomDatabase
transmitindo o contexto para instanciardatabase
. Use o delegadolazy
para que a instância dedatabase
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
mais adiante 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.
8. Adicionar um ViewModel
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 vai 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
.
Criar ViewModel do app Inventory
- No pacote
com.example.inventory
, crie uma classe do Kotlin com o nomeInventoryViewModel.kt
. - Estenda a classe
InventoryViewModel
daViewModel
. Transmita o objetoItemDao
como um parâmetro para o construtor padrão.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- No final do arquivo
InventoryViewModel.kt
fora da classe, adicione a classeInventoryViewModelFactory
para criar a instânciaInventoryViewModel
. Transmita o mesmo parâmetro do construtor da classeInventoryViewModel
, que é a instância deItemDao
. Estenda a classe daViewModelProvider.Factory
. O erro relacionado aos métodos não implementados será corrigido na próxima etapa.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- Clique na lâmpada vermelha e selecione Implement Members. Também é possível substituir o método
create()
na classeViewModelProvider.Factory
da forma apresentada abaixo, que aceita qualquer tipo de classe como argumento e retorna um objetoViewModel
.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
- Implemente o método
create()
. Verifique se amodelClass
é igual à da classeInventoryViewModel
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
)
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ê vai usar a tela Add Item para receber essas informações do usuário. Nesta tarefa, você vai 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.
- Na classe
InventoryViewModel
, adicione uma funçãoprivate
chamadainsertItem()
, que usa um objetoItem
e adiciona os dados ao banco de dados sem gerar bloqueios.
private fun insertItem(item: Item) {
}
- 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()
, useviewModelScope.launch
para iniciar uma corrotina noViewModelScope
. Dentro da função de inicialização, chame a função de suspensãoinsert()
noitemDao
, transmitindo oitem
. AViewModelScope
é uma propriedade de extensão para a classeViewModel
que cancela automaticamente as corrotinas filhas quando aViewModel
é destruída.
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.
- Na classe
InventoryViewModel
, adicione outra função particular que use três strings e retorne uma instânciaItem
.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- Ainda na classe
InventoryViewModel
, adicione uma função pública chamadaaddNewItem()
, que aceita três strings de detalhes do item. Transmita as strings de detalhes do item para a funçãogetNewItemEntry()
e atribua o valor retornado a um valor chamadonewItem
. Faça uma chamada parainsertItem()
transmitindo onewItem
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ê vai atualizar o fragmento Add Item para usar as funções acima.
9. Atualizar o AddItemFragment
- Em
AddItemFragment.kt
, no início da classeAddItemFragment
, crie umprivate val
chamadoviewModel
, do tipoInventoryViewModel
. Use o delegado de propriedadeby activityViewModels()
do Kotlin para compartilhar oViewModel
entre os fragmentos Você corrigirá o erro na próxima etapa.
private val viewModel: InventoryViewModel by activityViewModels {
}
- Dentro da lambda, chame o construtor
InventoryViewModelFactory()
e transmita a instânciaItemDao
. Use a instânciadatabase
, criada em uma das tarefas anteriores, para chamar o construtoritemDao
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
- Abaixo da definição do
viewModel
, crie umlateinit var
chamadoitem
, do tipoItem
.
lateinit var item: Item
- 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ê vai 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 classeInventoryViewModel
, adicione a seguinte funçãopublic
chamadaisEntryValid()
.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
- Em
AddItemFragment.kt
, abaixo da funçãoonCreateView()
, crie uma funçãoprivate
, chamadaisEntryValid()
, que retorna umBoolean
. O erro que indica a ausência de valor de retorno será corrigido na próxima etapa.
private fun isEntryValid(): Boolean {
}
- Implemente a função
isEntryValid()
na classeAddItemFragment
. Chame a funçãoisEntryValid()
na instância deviewModel
, transmitindo o texto das visualizações de texto. Retorne o valor da funçãoviewModel.isEntryValid()
.
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
- Na classe
AddItemFragment
, abaixo da funçãoisEntryValid()
, adicione outra funçãoprivate
chamadaaddNewItem()
, que não tem parâmetros e não retorna nada. Dentro da função, chameisEntryValid()
na condiçãoif
.
private fun addNewItem() {
if (isEntryValid()) {
}
}
- No bloco
if
, chame o métodoaddNewItem()
na instânciaviewModel
. Transmita os detalhes do item inseridos pelo usuário e use a instânciabinding
para fazer a leitura deles.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
- Abaixo do bloco
if
, crie umval
action
para voltar aoItemListFragment
. ChamefindNavController
().navigate()
transmitindo aaction
.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
Importe androidx.navigation.fragment.findNavController.
.
- 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)
}
}
- Para juntar tudo, adicione um gerenciador de cliques ao botão Save. Na classe
AddItemFragment
, acima da funçãoonDestroyView()
, modifique a funçãoonViewCreated()
. - Dentro da função
onViewCreated()
, adicione um gerenciador de cliques ao botão "Save" e chameaddNewItem()
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- 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.
Visualizar o banco de dados usando o Database Inspector
- 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.
- No Android Studio, selecione View > Tool Windows > Database Inspector na barra de menus.
- No painel do Database Inspector, selecione
com.example.inventory
no menu suspenso. - 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.
- 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.
Parabéns! Você criou um app que consegue persistir dados usando o Room. No próximo codelab, você adicionará uma RecyclerView
ao app para mostrar os itens do banco de dados e adicionará novos recursos, como exclusão e atualização de entidades. Até lá!
10. 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.
Para encontrar o código deste codelab e abrir no Android Studio, faça o seguinte.
Acessar o código
- Clique no URL fornecido. Isso abre a página do GitHub referente ao projeto em um navegador.
- Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma caixa de diálogo.
- Na caixa de diálogo, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
- Localize o arquivo no computador, que provavelmente está na pasta Downloads.
- Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.
Abrir o projeto no Android Studio
- Inicie o Android Studio.
- Na janela Welcome to Android Studio, clique em Open an existing Android Studio project.
Observação: caso o Android Studio já esteja aberto, selecione a opção File > New > Import Project.
- Na caixa de diálogo Import Project, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
- Clique duas vezes nessa pasta do projeto.
- Aguarde o Android Studio abrir o projeto.
- Clique no botão Run
para criar e executar o app. Confira se ele é compilado da forma esperada.
- Procure os arquivos do projeto na janela de ferramentas Project para ver como o app está configurado.
11. Resumo
- 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.
12. Saiba mais
Documentação do desenvolvedor Android
- Salvar dados em um banco de dados local usando Room
- androidx.room
- Depurar seu banco de dados com o Database Inspector
Postagens do blog
- Sete dicas para o Room (link em inglês)
- O único objeto. Vocabulário do Kotlin (link em inglês)
Vídeos
- Kotlin: como usar APIs Kotlin do Room (em inglês)
- Database Inspector (em inglês)
Outros documentos e artigos
- Padrão Singleton (link em inglês)
- Objetos complementares (link em inglês)
- Tutorial do SQLite: uma forma fácil de dominar o SQLite rapidamente (link em inglês)