Introdução

A maioria dos aplicativos possui dados que precisam ser mantidos, mesmo depois que o usuário fecha o aplicativo. Por exemplo, o aplicativo pode armazenar uma lista de reprodução, um inventário de itens do jogo, registros de despesas e receitas, um catálogo de constelações ou dados de sono ao longo do tempo. Normalmente, você usaria um banco de dados para armazenar dados persistentes.

Room é uma biblioteca de banco de dados que faz parte do Android Jetpack. Room cuida de muitas das tarefas de instalação e configuração de um banco de dados e torna possível que seu aplicativo interaja com o banco de dados usando chamadas de função comuns. Por baixo do capô, o Room é uma camada de abstração sobre um banco de dados SQLite. A terminologia da Room e a sintaxe de consulta para consultas mais complexas seguem o modelo SQLite.

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

O que você já deveria saber

Você deve estar familiarizado com:

O que aprenderá

O que fará

Neste tutorial, você constrói a parte do banco de dados de um aplicativo que rastreia a qualidade do sono. O aplicativo usa um banco de dados para armazenar dados de sono ao longo do tempo.

O aplicativo possui duas telas, representadas por fragmentos, conforme mostrado na figura abaixo.

A primeira tela, mostrada à esquerda, possui botões para iniciar e parar o rastreamento. A tela mostra todos os dados de sono do usuário. O botão Clear exclui permanentemente todos os dados que o aplicativo coletou para o usuário.

A segunda tela, mostrada à direita, é para selecionar uma classificação de qualidade do sono. No aplicativo, a classificação é representada numericamente. Para fins de desenvolvimento, o aplicativo mostra os ícones de rosto e seus equivalentes numéricos.

O fluxo do usuário é o seguinte:

Este aplicativo usa uma arquitetura simplificada, conforme mostrado abaixo no contexto da arquitetura completa. O aplicativo usa apenas os seguintes componentes:

Etapa 1: Baixe e execute o aplicativo inicial

  1. Baixe o aplicativo TrackMySleepQuality-Starter do GitHub.
  2. Construa e execute o aplicativo. O aplicativo mostra a IU para o fragmento SleepTrackerFragment, mas nenhum dado. Os botões não respondem aos toques.

Etapa 2: Inspecione o aplicativo inicial

  1. Dê uma olhada nos arquivos Gradle:
  1. Dê uma olhada nos pacotes e IU. O aplicativo é estruturado por funcionalidade. O pacote contém espaço reservado para arquivos onde você adicionará código em toda esta série de tutoriais.
  1. Dê uma olhada no arquivo Util.ktfile, que contém funções para ajudar a exibir dados de qualidade do sono. Algum código está comentado, pois faz referência a um modelo de vistas que você cria posteriormente.
  2. Dê uma olhada na pasta androidTest folder (SleepDatabaseTest.kt). Você usará este teste para verificar se o banco de dados funciona conforme o esperado.

No Android, os dados são representados em classes de dados, e os dados são acessados ​​e modificados usando chamadas de função. No entanto, no mundo do banco de dados, você precisa de entidades e consultas.

A Room faz todo o trabalho duro para você ir das classes de dados Kotlin às entidades que podem ser armazenadas em tabelas SQLite e de declarações de funções a consultas SQL.

Você deve definir cada entidade como uma classe de dados anotada e as interações como uma interface anotada, um objeto de acesso a dados (DAO). Room usa essas classes anotadas para criar tabelas no banco de dados e consultas que atuam no banco de dados.

Etapa 1: Crie a entidade SleepNight

Nesta tarefa, você define uma noite de sono como uma classe de dados anotada.

Por uma noite de sono, você precisa registrar a hora de início, hora de término e uma classificação de qualidade.

E você precisa de uma ID para identificar exclusivamente a noite.

  1. No pacote database, encontre e abra o arquivo SleepNight.kt.
  2. Crie a classe de dados SleepNight com parâmetros para um ID, uma hora de início (em milissegundos), uma hora de término (em milissegundos) e uma classificação numérica da qualidade do sono.
data class SleepNight(
       var nightId: Long = 0L,
       val startTimeMilli: Long = System.currentTimeMillis(),
       var endTimeMilli: Long = startTimeMilli,
       var sleepQuality: Int = -1
)
  1. Antes da declaração da classe, anote a classe de dados com @Entity. Nomeie a tabela como daily_sleep_quality_table. O argumento para tableName é opcional, mas recomendado. Você pode procurar outros argumentos na documentação.

    Se solicitado, importe Entity e todas as outras anotações da biblioteca androidx.
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(...)
  1. Para identificar o nightId como a chave primária, anote a propriedade nightId com @PrimaryKey. Defina o parâmetro autoGenerate como true para que Room gere o ID para cada entidade. Isso garante que o ID de cada noite seja único.
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,...
  1. Anote as propriedades restantes com @ColumnInfo. Personalize os nomes das propriedades usando os parâmetros mostrados abaixo.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
       @PrimaryKey(autoGenerate = true)
       var nightId: Long = 0L,

       @ColumnInfo(name = "start_time_milli")
       val startTimeMilli: Long = System.currentTimeMillis(),

       @ColumnInfo(name = "end_time_milli")
       var endTimeMilli: Long = startTimeMilli,

       @ColumnInfo(name = "quality_rating")
       var sleepQuality: Int = -1
)
  1. Crie e execute seu código para garantir que não haja erros.

Nesta tarefa, você define um objeto de acesso a dados (DAO). No Android, o DAO fornece métodos convenientes para inserir, excluir e atualizar o banco de dados.

Ao usar um banco de dados Room, você consulta o banco de dados definindo e chamando funções Kotlin em seu código. Essas funções Kotlin são mapeadas para consultas SQL. Você define esses mapeamentos em um DAO usando anotações e Room cria o código necessário.

Pense em um DAO como definindo uma interface personalizada para acessar seu banco de dados.

Para operações de banco de dados comuns, a biblioteca Room oferece anotações de conveniência, como @Insert, @Delete e @Update. Para todo o resto, existe a anotação @Query. Você pode escrever qualquer consulta que seja compatível com SQLite.

Como um bônus adicional, conforme você cria suas consultas no Android Studio, o compilador verifica suas consultas SQL em busca de erros de sintaxe.

Para o banco de dados do rasthreador de sono de noites de sono, você precisa ser capaz de fazer o seguinte:

Etapa 1: Crie o SleepDatabase DAO

  1. No pacote database, abra SleepDatabaseDao.kt.
  2. Observe que interface SleepDatabaseDao está anotada com @Dao. Todos os DAOs precisam ser anotados com a palavra-chave @Dao.
@Dao
interface SleepDatabaseDao {}
  1. Dentro do corpo da interface, adicione uma anotação @Insert. Abaixo de @Insert, adicione uma função insert() que leva uma instância da classe Entity SleepNight como seu argumento.

    É isso. Room gerará todo o código necessário para inserir o SleepNight no banco de dados. Quando você chama insert() do seu código Kotlin, Room executa uma consulta SQL para inserir a entidade no banco de dados. (Observação: Você pode chamar a função de qualquer maneira).
@Insert
fun insert(night: SleepNight)
  1. Adicione uma anotação @Update com uma função update() para um SleepNight. A entidade que é atualizada é a entidade que possui a mesma chave daquela que foi passada. Você pode atualizar algumas ou todas as outras propriedades da entidade.
@Update
fun update(night: SleepNight)

Não há anotação de conveniência para a funcionalidade restante, então você deve usar a anotação @Query e fornecer consultas SQLite.

  1. Adicione uma anotação @Query com uma função get() que leva uma função Long key argumento e retorna um SleepNight anulável. Você verá um erro para um parâmetro ausente.
@Query
fun get(key: Long): SleepNight?
  1. A consulta é fornecida como um parâmetro de string para a anotação. Adicione um parâmetro a @Query. Torne-a uma String que é uma consulta SQLite.
("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
  1. Adicione outra @Query com uma função clear() e uma consulta SQLite para usar DELETE em toda daily_sleep_quality_table. Esta consulta não exclui a própria tabela.

    A anotação @Delete exclui um item, e você pode usar @Delete e fornecer uma lista de noites a serem excluídas. A desvantagem é que você precisa buscar ou saber o que está na tabela. A anotação @Delete é ótima para excluir entradas específicas, mas não é eficiente para limpar todas as entradas de uma tabela.
@Query("DELETE FROM daily_sleep_quality_table")
fun clear()
  1. Adicione um @Query com uma função getTonight(). Torne o SleepNight retornado por getTonight() anulável, de modo que a função possa tratar com o caso em que a tabela está vazia. (A tabela está vazia no início e depois que os dados são apagados).

    Para obter "hoje à noite" do banco de dados, escreva uma consulta SQLite que retorne o primeiro elemento de uma lista de resultados ordenados por nightId em ordem decrescente. Use LIMIT 1 para retornar apenas um elemento.
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?
  1. Adicione um @Query com uma função getAllNights():
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
  1. Embora você não veja nenhuma mudança visível, execute seu aplicativo para verificar se não há erros.

Nesta tarefa, você cria um banco de dados Room que usa a Entity e o DAO que você criou na tarefa anterior.

Você precisa criar uma classe de recipiente de banco de dados abstrata, anotada com @Database. Essa classe tem um método que cria uma instância do banco de dados se o banco de dados não existir ou retorna uma referência a um banco de dados existente.

Obtendo um banco de dados Room é um pouco complicado, então aqui está o processo geral antes de começar com o código:

Etapa 1: Crie o banco de dados

  1. No pacote database, abra SleepDatabase.kt.
  2. No arquivo, crie uma classe abstract chamada SleepDatabase que estende RoomDatabase.

    Anote a classe com @Database.
@Database()
abstract class SleepDatabase : RoomDatabase() {}
  1. Você verá um erro para entidades ausentes e parâmetros de versão. A anotação @Database requer vários argumentos, para que Room possa construir o banco de dados.
entities = [SleepNight::class], version = 1, exportSchema = false
  1. O banco de dados precisa saber sobre o DAO. Dentro do corpo da classe, declare um valor abstrato que retorna o SleepDatabaseDao. Você pode ter vários DAOs.
abstract val sleepDatabaseDao: SleepDatabaseDao
  1. Abaixo disso, defina um objeto companion. O objeto complementar permite que os clientes acessem os métodos para criar ou obter o banco de dados sem instanciar a classe. Visto que o único propósito desta classe é fornecer um banco de dados, não há razão para instanciá-lo.
 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 para o banco de dados, uma vez criada. Isso ajuda a evitar abrir repetidamente conexões com o banco de dados, o que é caro.

Anote INSTANCE com @Volatile. O valor de uma variável volátil nunca será armazenado em cache e todas as gravações e leituras serão feitas de e para a memória principal. Isso ajuda a garantir que o valor de INSTANCE esteja sempre atualizado e o mesmo para todos as threads de execução. Isso significa que as alterações feitas por uma thread em INSTANCE são visíveis para todos as outras threads imediatamente, e você não obtém uma situação em que, digamos, dois threads atualizam cada uma a mesma entidade em um cache, o que criar um problema.

@Volatile
private var INSTANCE: SleepDatabase? = null
  1. Abaixo de INSTANCE, ainda dentro do objeto companion, defina um método getInstance() com um parâmetro Context que o banco de dados o construtor precisará. Retorne um tipo SleepDatabase. Você verá um erro, pois getInstance() ainda não está retornando nada.
fun getInstance(context: Context): SleepDatabase {}
  1. Dentro de getInstance(), adicione um bloco synchronized{}. Passe this para que você possa acessar o contexto.

    Várias threads podem solicitar uma instância do banco de dados ao mesmo tempo, resultando em dois bancos de dados em vez de um. É improvável que esse problema aconteça neste aplicativo de amostra, mas é possível para um aplicativo mais complexo. Encapsulando o código para obter o banco de dados em synchronized significa que apenas uma thread de execução por vez pode entrar neste bloco de código, o que garante que o banco de dados seja inicializado apenas uma vez.
synchronized(this) {}
  1. Dentro do bloco sincronizado, copie o valor atual de INSTANCE para uma variável local instance. Isso é para tirar vantagem da conversão inteligente, que somente está disponível para variáveis ​​locais.
var instance = INSTANCE
  1. Dentro do bloco synchronized, return instance no final do bloco synchronized. Ignore o erro de incompatibilidade de tipo de retorno; você nunca retornará nulo quando terminar.
return instance
  1. Acima da instrução return, adicione uma instrução if para verificar se a instance é nula, ou seja, ainda não há banco de dados.
if (instance == null) {}
  1. Se instance for null, use o construtor de banco de dados para obter um banco de dados. No corpo da instrução if, invoque Room.databaseBuilder e forneça o contexto que você passou, a classe do banco de dados e um nome para o banco de dados, sleep_history_database. Para remover o erro, você terá que adicionar uma estratégia de migração e build() nas etapas a seguir.
instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database")
  1. Inclua a estratégia de migração necessária no construtor. Use .fallbackToDestructiveMigration().

    Normalmente, você teria que fornecer um objeto de migração com uma estratégia de migração para quando o esquema fosse alterado. Um objeto de migração é um objeto que define como você pega todas as linhas com o esquema antigo e as converte em linhas no novo esquema, para que nenhum dado seja perdido. Migração está além do escopo deste tutorial. Uma solução simples é destruir e reconstruir o banco de dados, o que significa que os dados serão perdidos.
.fallbackToDestructiveMigration()
  1. Finalmente, chame .build().
.build()
  1. Atribua INSTANCE = instance como a etapa final dentro da instrução if.
INSTANCE = instance
  1. Seu código final deve ser semelhante a este:
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {

   abstract val sleepDatabaseDao: SleepDatabaseDao

   companion object {

       @Volatile
       private var INSTANCE: SleepDatabase? = null

       fun getInstance(context: Context): SleepDatabase {
           synchronized(this) {
               var instance = INSTANCE

               if (instance == null) {
                   instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database"
                   )
                           .fallbackToDestructiveMigration()
                           .build()
                   INSTANCE = instance
               }
               return instance
           }
       }
   }
}
  1. Construa e execute seu código.

Agora você tem todos os blocos de construção para trabalhar com o banco de dados do Room. Este código é compilado e executado, mas você não tem como saber se ele realmente funciona. Portanto, este é um bom momento para adicionar alguns testes básicos.

Etapa 2: Teste o SleepDatabase

Nesta etapa, você executa os testes fornecidos para verificar se o banco de dados funciona. Isso ajuda a garantir que o banco de dados funcione antes de você construí-lo. Os testes fornecidos são básicos. Para um aplicativo de produção, você exercitaria todas as funções e consultas em todos os DAOs.

O aplicativo inicial contém uma pasta androidTest. Esta pasta androidTest contém testes de unidade que envolvem instrumentação Android, que é uma maneira sofisticada de dizer que os testes precisam da estrutura Android, portanto, você precisa executar os testes em um dispositivo físico ou virtual. Claro, você também pode criar e executar testes de unidade puros que não envolvem a estrutura do Android.

  1. No Android Studio, na pasta androidTest, abra o arquivo SleepDatabaseTest.
  2. Para descomentar o código, selecione todos os códigos comentados e pressione o atalho de teclado Cmd+/ ou Control+/.
  3. Dê uma olhada no arquivo.

Aqui está uma rápida execução do código de teste, pois é outra parte do código que você pode reutilizar:

  1. Clique com o botão direito do mouse no arquivo de teste no painel Project e selecione Run 'SleepDatabaseTest'.
  2. Após a execução dos testes, verifique no painel SleepDatabaseTest se todos os testes foram aprovados.

Como todos os testes foram aprovados, agora você sabe vários assuntos:

Projeto Android Studio: TrackMySleepQualityRoomAndTesting

Ao testar um banco de dados, você precisa exercitar todos os métodos definidos no DAO. Para concluir o teste,adicione e execute testes para exercitar os outros métodos DAO.

Documentação do desenvolvedor Android:

Outra documentação e artigos:

Esta seção lista as possíveis tarefas de casa para os alunos que estão trabalhando neste tutorial como parte de um curso ministrado por um instrutor.

Responda a essas perguntas

Pergunta 1

Como você indica que uma classe representa uma entidade para armazenar em um banco de dados Room?

Pergunta 2

O DAO (objeto de acesso a dados) é uma interface que o Room usa para mapear funções Kotlin para consultas de banco de dados.

Como você indica que uma interface representa um DAO para um banco de dados Room?

Pergunta 3

Quais das afirmações a seguir são verdadeiras sobre o banco de dados Room? Escolha todas as opções aplicáveis.

Pergunta 4

Qual das seguintes anotações você pode usar em sua interface @Dao? Escolha todas as opções aplicáveis.

Pergunta 5

Como você pode verificar se seu banco de dados está funcionando? Selecione tudo que se aplica.

Comece para a próxima lição: 06.2: Corrotinas e Room

Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.