Android Architecture Components com Kotlin: Persistindo dados com Room

Alex Felipe
CollabCode
Published in
9 min readMay 21, 2018

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 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 😃

--

--