Alex Felipe
May 21, 2018 · 9 min read

Atualmente é muito comum desenvolvermos Apps Android que funcionam no modo offline, ou seja, que armazenam dados internamente. Existem diversas técnicas que possibilitam esse comportamento, como por exemplo, o SQLite.

Entretanto, a abordagem inicial para persistir dados via SQLite, obriga o desenvolvedor a escrever um boilerplate para construir uma estrutura de banco de dados, definir tabelas e colunas com suas regras… Resumindo:

Escrevemos muito código para persistir dados na App!

Pensando em melhorar essa experiência como desenvolvedor, a equipe da Google lançou o Room, uma lib capaz de realizar a persistência de dados com o SQLite, porém, utilizando o conceito de ORM! 😃

“Muito legal, mas como o Room funciona e como posso utilizá-lo?”

Como toda nova tecnologia, o setup inicial é sempre o maior desafio, portanto, neste artigo, vamos aprender como configuramos o Room e faremos operações básicas, como salvar e buscar dados.

Preparado? Então bora começar.

Bob Esponja fazendo o ‘joinha’

Este artigo faz parte da nova série Android Architecture Components com Kotlin que tem o objetivo de agregar todo o conteúdo relacionado aos novos componentes da Google para uma arquitetura de Apps mais robustas, testáveis e fácil manutenção. Confira:


Projeto de exemplo

Durante este artigo, utilizaremos o projeto TechStore (uma simulação de loja virtual de produtos de tecnologia) que servirá como base para aplicarmos o Room. Para baixá-lo você pode acessar o repositório via GitHub:

Ao rodar o projeto, deve aparecer uma tela em branco com um FloatingActionButton, ao tocá-lo, aparece o formulário que salva produtos, então, após salvar um produto, volta para a tela anterior com o produto em uma lista:

Comportamento base da App

A implementação atual funciona, mas de maneira estática, ou seja, quando fechamos a App todos os dados são perdidos!

Em outras palavras, o nosso objetivo é configurar o Room para substituir essa abordagem inicial e manter os produtos salvos permanentemente. Agora que sabemos o nosso objetivo, podemos começar com a configuração inicial.

Adicionando o Room no projeto

Como primeiro passo, precisamos adicionar o Room no projeto, para isso, adicionamos as seguintes instruções no build.gradle do módulo app e sincronizar o projeto:

Perceba que usamos o kapt que é o Annotation Processor para Kotlin.

Já somos capazes de utilizar o Room! Mas agora vem a questão:

“Por onde podemos começar a configuração?”

Antes de escrever qualquer código, vamos entender a base de funcionamento do Room para uma melhor compreensão.

Conceito básico de funcionamento do Room

O Room é composto de 3 principais componentes:

  • Database: responsável em criar instâncias capazes de conectar com o banco de dados e fornecê-las para a App;
  • DAO: mantém todas as funções que interagem com o banco de dados, como salvar ou listar os dados;
  • Entity: representa a tabela que será criada.

A princípio pode parecer bastante abstrato, mas, existe uma relação forte relação entre os componentes para que tudo funcione como esperado. A própria página de treinamento da Google, fornece o seguinte diagrama que descreve esse relacionamento:

Diagrama da arquitetura do Room

Neste diagrama, o Room Database representa o Database (Banco de dados) que, por sua vez, fornece uma instância do DAO (Objeto de Acesso de Dados) que é responsável pelas operações com o banco de dados baseando-se no Entity (Entidade), que mantém as informações do nosso modelo.

Perceba que cada componente tem uma responsabilidade única e que, tanto o Database como o DAO, mantém uma dependência de componentes. Sendo assim, podemos começar a configuração a partir Entity que não depende de nenhum componente.

Configurando o componente Entity

A configuração de componentes é feita a partir de annotations, portanto, para transformarmos o nosso produto em um Entity, basta apenas anotá-lo com @Entity:

package alexf.com.br.techstore.model

import android.arch.persistence.room.Entity

@Entity
class Product(
val name: String,
val description: String,
val quantity: Int)

Definindo a primary key

Todo Entity precisa definir uma property como primary key, O Room fornece a annotation @PrimaryKey para essa configuração. Podemos considerar qualquer tipo primário, mas, por ser comum em banco de dados o uso de números inteiros incrementais e únicos, vamos utilizar o tipo Long:

@Entity
class Product(
@PrimaryKey
val id: Long,
val name: String,
val description: String,
val quantity: Int)

Além de anotar com @PrimaryKey, para delegar a responsabilidade de incrementar o valor, precisamos definir o argumento autoGenerate com valor true:

@PrimaryKey(autoGenerate = true)
val id: Long

Com essa modificação a FormProductActivity para de compilar, pois a função create() faz uma instância de Product sem enviar o id. Esse comportamento deve ser mantido, afinal, o nosso banco de dados que deve gerar esse valor pra gente! Nesse caso, podemos definir um valor padrão para o id:

@PrimaryKey(autoGenerate = true)
val id: Long = 0

“Se mantermos o valor padrão com 0, não teremos problemas quando salvarmos o produto?”

Essa é a primeira preocupação que temos com essa abordagem, porém, a própria documentação do @PrimaryKey indica que, ao aplicarmos o autoGenerate, se o campo for do tipo long ou int o valor 0 será considerado como não inserido, e então, um id novo será gerado durante a inserção.

Pronto, finalizamos a configuração do componente Entity, podemos então começar com o componente DAO que depende apenas do Entity.

Configurando o componente DAO

A implementação do DAO é feita a partir de uma interface que mantém as assinaturas com as ações que serão realizadas com o banco de dados. Sendo assim, vamos modificar a nossa classe ProductDao:

package alexf.com.br.techstore.dao

import alexf.com.br.techstore.model.Product

interface ProductDao {

fun all(): List<Product>

fun add(vararg product: Product)

}

Perceba que agora não temos nenhuma implementação! Porém, já que a configuração é feita a partir de annotation processor, anotamos a interface com @Dao para que o Room a identifique como um DAO:

@Dao
interface ProductDao

Da mesma maneira, cada uma das assinaturas precisam de annotations para que o Room gere o código com o comportamento esperado. Existem 3 possibilidades que podemos utilizar:

  • @Query: permite definir uma query com o banco de dados como já fazíamos com o SQLite;
  • @Insert: salva todas entidades (objetos representados como Entity) recebidos como parâmetro;
  • @Delete: realiza o mesmo procedimento do @Insert a diferença é que remove as entidades.

Baseando-se nas funcionalidades esperadas, vamos utilizar o @Query para buscar todos os produtos e o @Insert para inserir os produtos:

package alexf.com.br.techstore.dao

import alexf.com.br.techstore.model.Product
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query

@Dao
interface ProductDao {

@Query("SELECT * FROM product")
fun all(): List<Product>

@Insert
fun add(vararg product: Product)

}

Uma observação bacana: a partir da versão 3.1 do Android Studio, temos diversas features que auxiliam no uso do Room, como é o caso da verificação de syntax e o auto complete:

Demonstração a verificação de syntax e auto complete

Dessa maneira, evitamos diversos problemas durante a escrita de SQL, incrível, né? 😄


Pronto, configuramos o DAO, vamos agora para a configuração do último componente, o Database.

Configurando o database

A configuração do Database é feita a partir de uma classe abstrata que herda da classe RoomDatabase, portanto, vamos criar a AppDatabase que vai representar o banco de dados do nosso projeto:

package alexf.com.br.techstore.database

import android.arch.persistence.room.RoomDatabase

abstract class AppDatabase : RoomDatabase() {
}

Além disso, a própria documentação indica que toda a classe que herda diretamente de RoomDatabase, precisa ser anotada com @Database:

@Database
abstract class AppDatabase : RoomDatabase()

Perceba que nesse momento a annotation para de compilar, exigindo que enviemos os parâmetros:

  • entities: array de entidades que serão incluídas pelo Room a partir do Database;
  • version: versão do banco de dados para permitir o uso de migration.

Sendo assim, vamos enviar os parâmetros esperados:

@Database(entities = [Product::class], version = 1)
abstract class AppDatabase : RoomDatabase()

Por fim, precisamos apenas adicionar uma função abstrata que devolve o nosso DAO:

package alexf.com.br.techstore.database

import alexf.com.br.techstore.database.dao.ProductDao
import alexf.com.br.techstore.model.Product
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase

@Database(entities = [Product::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
}

Perceba que modifiquei o pacote do ProductDao para que seja um subpacote de database.

Lidando com os problemas de compilação

Finalizamos a configuração de todos os componentes! Entretanto, ao construir o projeto novamente, tivemos alguns problemas de compilação. Em outras palavras, vamos analisar cada um deles e resolvê-los.

Problema com o Schema export directory

Primeiro, vamos focar neste aqui:

path_project/TechStore/app/build/tmp/kapt3/stubs/debug/alexf/com/br/techstore/database/AppDatabase.java:7: warning: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.
public abstract class AppDatabase extends android.arch.persistence.room.RoomDatabase {

Ele indica que o diretório de exportação de esquema não é fornecido para o annotation processor, então ele sugere ou fornecer um arquivo ou configurar o parâmetro exportSchame como false.

Considerando que é a primeira vez que configuramos o Room e não temos o arquivo, neste momento faz todo o sentido utilizarmos a segunda abordagem:

@Database(entities = [Product::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase()

É importante lembrar que a documentação sugere o uso do exportSchema como uma boa prática, você pode acessá-la para mais detalhes.

Construíndo o projeto novamente, esse erro deixa de aparecer, porém o erro de compilação por conta das referências do ProductDao em ambas Activities ainda aparecem!

Criando as instâncias para o DAO

Como vimos, agora temos interfaces para o ProductDao isso significa que elas precisam ser implementadas. Já que o Room ficou responsável por isso, vamos utilizar a função estática databaseBuilder() da classe Room que é capaz de criar as instâncias do DAO pra gente. Ela exige 3 argumentos:

  • Context: contexto que vai ser utilizado;
  • klass: referência da classe que foi representada como database;
  • name: nome do arquivo que vai ser gerado para o banco de dados.

Temos que criar uma referência que precisa de Context, ou seja, dentro da Activity, precisamos realizar esse passo no onCreate(). Portanto, vamos deixar a configuração da seguinte maneira:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_products_list)
val database = Room.databaseBuilder(
this,
AppDatabase::class.java,
"techstore-database")
// rest of the code
}

Por fim, precisamos apenas chamar a função build() para criar o Database e atribuir para a property, então, podemos inicializar a property productDao a partir da função abstrata do database:

private lateinit var productDao: ProductDao
private lateinit var adapter: ProductsListAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_products_list)
val database = Room.databaseBuilder(
this,
AppDatabase::class.java,
"techstore-database")
.build()
productDao = database.productDao()
// rest of the code
}

Essa solução pode ser aplicada em ambas Activities. Construíndo novamente o projeto, tudo volta a compilar e podemos testar o projeto. Entretanto, logo quando a App abre ocorre um erro!

“O que aconteceu?”

Veja que agora temos o seguinte erro:

Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Isso significa que o Room, por padrão, impede qualquer tipo de operação que envolva a execução direta na UI Thread do Android. Sendo assim, temos duas alternativas para lidar com esse tipo de situação:

  • Executar as operações com o banco de dados de maneira assíncrona, como por exemplo, a partir de uma AsyncTask;
  • Desabilitar esse comportamento padrão e permitindo a execução de maneira síncrona, como fizemos agora.

Como o objetivo deste artigo não é discutir sobre boas práticas e detalhes peculiares do uso do Room, vamos considerar a segunda abordagem. Para isso, basta apenas chamar a função allowMainThreadQueries() durante o processo de construção do Database:

val database = Room.databaseBuilder(
this,
AppDatabase::class.java,
"techstore-database")
.allowMainThreadQueries()
.build()

Testando novamente a App tudo funciona como antes, a diferença é que agora os dados estão sendo mantidos mesmo que a App seja fechada! 😃

Para aprender mais

Durante este post, foquei apenas na configuração base para o funcionamento do Room, ou seja, existem diversos pontos que podem ser melhorados, sendo assim, vou listá-los para que você tenha consciência e possa pesquisar após leitura:

  • Apenas uma instância global e imutável do Database é o suficiente para ser usada em todos os pontos do projeto (o uso de injeção de dependência é válido também);
  • O nome do arquivo do banco de dados faz mais sentido como uma constante para evitar a mudança de valor;
  • Para execuções assíncronas, considere o uso do LiveData, RxJava ou coroutine. Ambos possuem uma syntax mais enxuta que o AsyncTaks, porém, para determinadas situações o LiveData pode ser mais benéfico, recomendo consultar a documentação.

Com certeza são pontos que valem um novo artigo. Mas, já que ele ainda não foi produzido, deixo por sua conta a pesquisa 😉

Código fonte do projeto

Caso surgiu alguma dúvida ou queira apenas consultar o projeto, fique à vontade em dar uma olhada no repositório do GitHub:

Conclusão

Neste artigo aprendemos como podemos adicionar e configurar o Room em um projeto Android. Vimos que a base dele é feita por 3 componentes: Entity, DAO e Database que são necessários para que tudo funcione como esperado.

Além disso, tivemos a oportunidade de resolver detalhes durante a configuração, como erros de compilação e também o famoso problema de não poder executar o Room na UI Thread.

Aproveitando a sua atenção, me conte o que achou do Room nos comentários 😃

CollabCode

Aqui é o ponto de encontro entre quem quer aprender e quem pode ensinar, de forma colaborativa.

Alex Felipe

Written by

Instructor at Alura, Android and Spring Developer.

CollabCode

Aqui é o ponto de encontro entre quem quer aprender e quem pode ensinar, de forma colaborativa.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade