Banco de Dados Local Com a Room API - YouTuber Android App - Parte 6

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Banco de Dados Local Com a Room API - YouTuber Android App - Parte 6

Banco de Dados Local Com a Room API - YouTuber Android App - Parte 6

Vinícius Thiengo
(3244)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloDomain-driven Design Destilado
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição
Ano2024
Páginas160
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo vamos continuar com o nosso projeto de aplicativo Android para YouTubers.

Nesta parte seis do projeto nós vamos adicionar ao aplicativo uma persistência local, persistência de dados dinâmicos.

Isso, pois parte dos dados do app são dados consumidos de servidor remoto (YouTube) e assim é prudente dar também ao usuário a possibilidade de uso, navegação, do aplicativo quando offline.

Configuração de persistência local do projeto de app Android

Antes de prosseguir, saiba que:

A versão e-book (PDF 📙) do projeto completo está disponível somente para os inscritos da lista de e-mails 📧 do Blog.

Se você se inscrever ainda hoje é possível que o link do e-book esteja disponível na página de confirmação de sua inscrição na lista de e-mails do Blog.

A seguir os tópicos que estaremos abordando neste sexto conteúdo do projeto de aplicativo Android para YouTubers:

O que já temos até aqui

Se você chegou no projeto somente agora, saiba que este não é o primeiro e nem mesmo o último artigo já publicado sobre essa proposta de aplicativo Android.

Todo o roteiro de construção do projeto está na listagem a seguir:

Para tirar o máximo proveito do projeto de aplicativo que estaremos desenvolvendo... para isso é inteligente seguir cada um dos conteúdos na ordem apresentada na lista anterior.

Repositório

Para ter acesso a todos os códigos fontes do projeto já finalizado, entre no repositório GitHub dele em:

➙ GitHub do projeto de aplicativo Android para YouTuber.

Por que persistência local (Room)?

Confesso que a inúmeras APIs de persistência local que poderíamos estar utilizando.

Mas aqui foi inteligente utilizar a Room API, principalmente porque no Blog ainda não tínhamos algum conteúdo com ela, que é a API / biblioteca de persistência local mais atual hoje em dia para desenvolvimento de apps Android.

Antes de continuar vale ressaltar que estaremos utilizando os termos API e biblioteca como sinônimos, ok?

Entendendo a necessidade

Se a necessidade de persistência de nosso projeto de aplicativo fosse apenas os dados de um novo vídeo liberado em canal.

Sendo somente isso eu confesso que certamente optaria por utilizar um SharedPreferences.

Pois está API atenderia ao projeto e todo o código do app, ao menos o da parte de persistência, ficaria mais enxuto.

Porém, para a versão de app do canal YouTube Vinícius Thiengo, também teremos em persistência local e dinâmica os dados de PlayLists.

Tela de PlayLists do aplicativo Android

Somente neste canal são mais de 40 PlayLists. Dependendo do canal este número pode ser ainda maior.

Obviamente que se o canal no qual você estiver trabalhando não for em app necessária a tela de PlayLists, então seguramente você pode remover todos os códigos de PlayLists do projeto.

Eu até acredito que muitos canais pequenos não vão querer a listagem de PlayLists. Principalmente porque inúmeros canais não têm ao menos uma PlayList dos próprios vídeos.

O porquê da Room API

Sabendo da possibilidade de inúmeras PlayLists entrando em aplicativo...

... com isso já temos que o uso de um SharedPreferences é totalmente inviável.

Uma outra boa opção, nativa Android, seria o SQLite.

Mas a Room API foi integrada ao Android justamente para facilitar o trabalho com o SQLite. Ou seja, é exatamente o SQLite que é utilizado por "debaixo dos panos".

Sendo assim eu não vejo uma melhor escolha senão a própria Room API que, acredite, é realmente simples de trabalhar.

Configurando a persistência local

Com o porquê já externado, podemos seguramente partir para a configuração em código.

É muito importante que você leia todos os comentários presentes nos fontes além do conteúdo textual do artigo.

Todo o código que faz uso das APIs Room é bem intuitivo, provavelmente você não terá dificuldades em entende-lo se já chegou à este ponto do projeto.

Mas é aquilo: surgindo dúvidas, pode perguntar que logo eu lhe respondo.

Configuração em arquivo Gradle

No Gradle Nível de Aplicativo, ou build.gradle (Module: app), adicione ao topo deste arquivo de automação de projeto o plugin de processamento de anotação, o kotlin-kapt:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
...

 

Logo depois, ainda no mesmo arquivo de automação, coloque em dependencies a definição de importação da Room como a seguir:

...
dependencies {
...

/*
* Room API para persistência local eficiente utilizando
* "por debaixo dos panos" a persistência SQLite.
* */
def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}

 

Ao final sincronize o projeto.

Note que se na época em que você estiver implementando o projeto houver uma versão mais atual da Room API do que a versão 2.2.5 utilizada aqui...

... neste caso você deve optar pela versão mais atual da API.

O projeto deverá continuar funcionando sem problemas.

Classes de domínio

Diferentemente do roteiro de criação das outras classes de domínio do projeto.

Aqui teremos que criar as duas classes que representam a estrutura das duas tabelas que vamos ter em persistência local.

LastVideo

Sendo assim, no pacote /model do projeto, crie a classe LastVideo com o código a seguir:

package thiengo.com.br.canalvinciusthiengo.model

import android.net.Uri
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import thiengo.com.br.canalvinciusthiengo.config.YouTubeConfig

/**
* O último vídeo público disponível no canal
* YouTube do aplicativo.
*
* O objetivo desta classe (objeto desta classe)
* é manter os principais dados do último vídeo
* do canal para que o usuário do app consiga
* ter acesso imediato a ele.
*
* Outro objetivo desta classe é ser uma entidade
* (estrutura) para a persistência local, Room.
* Pois os dados do último vídeo são ou carregados de
* um servidor remoto, servidor do YouTube, ou de
* notificação push. E com a Room API é possível
* ainda permitir acesso do usuário ao vídeo mesmo
* quando o servidor do YouTube ou o sistema
* notificações push não mais retornaram respostas.
*
* @property uid identificador único do vídeo
* para acesso a ele no site ou aplicativo do
* YouTube e também na persistência local, Room
* API. É o mesmo identificador do vídeo no
* site do YouTube.
* @property title título do vídeo.
* @property description descrição do vídeo.
* @constructor cria um objeto completo do tipo
* [LastVideo].
*/
@Entity
data class LastVideo(
@PrimaryKey val uid: String,
@ColumnInfo( name = "title" ) val title: String,
@ColumnInfo( name = "description" ) val description: String = "" ){

/**
* @property thumbUrl contém a thumb URL
* do vídeo. O método set() foi sobrescrito
* para que sempre tenha uma URL válida de
* thumb de vídeo.
* @return a Web URL válida da thumb do
* vídeo.
*/
@ColumnInfo( name = "thumb_url" )
var thumbUrl: String = ""
set( value ) {
field = if( value.isNotEmpty() ){
value
}
else{
alternativeThumbUrl()
}
}

/**
* Retorna a Web URL alternativa da thumb do
* vídeo.
*
* É útil principalmente quando o novo último
* vídeo é enviado ao aplicativo por meio de
* notificação push. Pois a notificação
* carrega também como dado a URL do vídeo e
* não a URL da thumb.
*
* @return a Web URL da thumb do vídeo.
*/
private fun alternativeThumbUrl()
= String.format(
YouTubeConfig.Channel.VIDEO_THUMB_URL_TEMPLATE,
uid
)

/**
* Retorna a Web URL do vídeo. URL que deve
* ser acionada junto a um objeto [Intent] em
* uma invocação à startActivity().
*
* Assim o vídeo será aberto dentro do
* aplicativo nativo do YouTube ou no site
* oficial.
*
* @return a Web URL do vídeo.
*/
fun webUri()
= Uri.parse(
String.format(
YouTubeConfig.Channel.VIDEO_URL_TEMPLATE,
uid
)
)
}

 

Você provavelmente deve estar confuso com as seguintes entidades da classe LastVideo:

  • A propriedade description com um dado inicial igual a vazio ("");
  • A propriedade thumbUrl inicializada de maneira diferente (ela é mutável - var), incluindo a sobrescrita do método set() desta propriedade e o uso do método alternativeThumbUrl().

Para ambas as confusões a explicação é a mesma:

Quando os dados de "último vídeo liberado" em canal são consumidos da YouTube Data API (ainda configuraremos esse algoritmo em projeto), todos os dados das propriedades de um objeto LastVideo são retornados.

Porém, para o usuário dono do canal do aplicativo, seria pouco inteligente, por exemplo, solicitar a ele que obtivesse também a URL da thumb do vídeo e à acrescentasse à notificação de novo último vídeo...

... notificação que ficará na responsabilidade dele de criar.

Sendo assim, os únicos dados obrigatórios em notificação push criada pelo manager do canal serão:

➙ URL do vídeo;

➙ e Título do vídeo.

Descrição será opcional em notificação e a URL da thumb não será possível fornecer em notificação.

Pois não é nada trivial obtê-la quando a pessoa responsável por gerar a notificação não tem a obrigação de entender sobre "navegar em estrutura HTML de página".

Em resumo:

Toda a configuração "não ortodoxa" em LastVideo é porque a pessoa responsável por gerar a notificação push de novo vídeo não terá, muito provavelmente, conhecimentos de programação.

Sendo assim o nosso código em LastVideo tem que compensar isso.

E sim... ainda temos que configurar em projeto as constantes VIDEO_URL_TEMPLATE e VIDEO_THUMB_URL_TEMPLATE.

Assim, na classe YouTubeConfig coloque ambas as constantes com a seguinte configuração:

...
abstract class YouTubeConfig {

abstract class Channel {
companion object {
...
const val VIDEO_URL_TEMPLATE = "https://www.youtube.com/watch?v=%s"
const val VIDEO_THUMB_URL_TEMPLATE = "https://i.ytimg.com/vi/%s/hqdefault.jpg"
}
}
}

 

Aqui, o %s presente em ambas as Strings de constantes é o que faz elas receberem em rótulo o termo TEMPLATE.

Sobre as anotações:

@Entity

Indica que a classe é uma entidade, uma tabela, no banco de dados local Room.

@PrimaryKey

É obrigatória para a configuração de banco de dados via Room API que utilizaremos.

E como chave primária vamos utilizar o identificador único de vídeo que ou é retornado na chamada a YouTube Data API ou estará na URL de vídeo presente na notificação push criada.

@ColumnInfo

É utilizada para indicar que a propriedade "XYZ" representa a coluna que está definida como argumento da anotação. Aqui nós mantivemos os mesmos rótulos, mas isso não é obrigatório.

Vale informar que para a nossa configuração de banco de dados local todas as propriedades que representam colunas em banco de dados devem ser de acesso public.

É isso. Vamos a classe de PlayList.

PlayList

Ainda no pacote /model crie a classe PlayList com o código a seguir:

package thiengo.com.br.canalvinciusthiengo.model

import android.net.Uri
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import thiengo.com.br.canalvinciusthiengo.R
import thiengo.com.br.canalvinciusthiengo.config.YouTubeConfig

/**
* Uma PlayList disponível no canal YouTube vinculado
* ao aplicativo.
*
* O objetivo desta classe (objetos desta classe)
* é manter os principais dados da PlayList para
* que o usuário do app consiga ter acesso imediato
* a ela.
*
* Outro objetivo desta classe é ser uma entidade
* (estrutura) para a persistência local, Room.
* Pois os dados de PlayList são carregados de um
* servidor remoto, servidor do YouTube. E com a
* Room API é possível ainda permitir acesso do
* usuário à PlayList mesmo quando o servidor não
* mais retornou respostas.
*
* @property title nome da PlayList.
* @property uid identificador único da PlayList
* para acesso a ela no site ou aplicativo do
* YouTube e também na persistência local, Room
* API. É o mesmo identificador da PlayList no
* sistema do YouTube.
* @constructor cria um objeto completo do tipo
* [PlayList].
*/
@Entity
class PlayList(
@ColumnInfo( name = "title" ) val title: String,
@PrimaryKey val uid: String,
val thumb: Int = R.drawable.ic_playlist_color
) : ListItem {

override fun getMainText()
= title

override fun getAppUri()
= Uri.parse(
String.format(
YouTubeConfig.Channel.PLAYLIST_URL_TEMPLATE,
uid
)
)

override fun getIcon()
= thumb
}

 

Ainda é preciso atualizar YouTubeConfig com a constante PLAYLIST_URL_TEMPLATE.

Segue:

...
abstract class YouTubeConfig {

abstract class Channel {
companion object {
...
const val VIDEO_URL_TEMPLATE = "..."
const val VIDEO_THUMB_URL_TEMPLATE = "..."
const val PLAYLIST_URL_TEMPLATE = "https://www.youtube.com/playlist?list=%s"
}
}
}

 

Como a propriedade thumb não é parte da tabela de PlayList em banco de dados local, ela então não recebe nenhuma anotação da Room API.

Note que PlayList herda de ListItem, pois ao menos a classe adaptadora (ListItemAdapter) e a classe ViewHolder (ListItemViewHolder) de framework de lista criadas para o fragmento FrameworkListFragment...

... ao menos essas duas entidades nós estaremos sim reaproveitando no fragmento da tela de apresentação de PlayLists.

Definição dos DAOs

Com as classes @Entity definidas em projeto, podemos partir para a configuração das SQLs, mais precisamente das classes Data Object Access (DAO).

Antes, dentro do pacote /data, crie um novo pacote com o rótulo /dynamic.

DAO LastVideo

No novo pacote criado, desenvolva a Interface DAO LastVideoDao com a seguinte configuração:

package thiengo.com.br.canalvinciusthiengo.data.dynamic

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import thiengo.com.br.canalvinciusthiengo.model.LastVideo

/*
* Interface de configuração de acesso à tabela
* LastVideo da persistência local, Room API.
* */
@Dao
interface LastVideoDao {

/**
* Salva na base local o último vídeo
* liberado em canal.
*
* Caso tenha conflito de identificadores
* único (por ser o mesmo vídeo), então
* salva os dados recém obtidos por cima dos
* já presentes em base. É esse tipo de
* estratégia que mantém a thumb, o título
* e a descrição do vídeo em dia com o que
* há disponível em canal no YouTube.
*
* @param lastVideo vídeo.
*/
@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insert( lastVideo: LastVideo )

/**
* Retorna o único [LastVideo] salvo em Room
* (SQLite) ou null caso não tenha ainda
* algum.
*
* @return o único [LastVideo] em base.
*/
@Query( value = "SELECT * FROM LastVideo LIMIT 1" )
fun get() : LastVideo?

/**
* Remove todos os dados [LastVideo]
* presentes em base local. Isso deve ocorrer
* sempre antes de invocar o método
* insert() para garantir que somente um
* único conjunto de dados de "último
* vídeo" permaneça no aplicativo.
*/
@Query( value = "DELETE FROM LastVideo" )
fun delete()
}

 

E sim. Não se espante...

... não haverá método update.

Estaremos trabalhando com apenas dois tipos de dados (LastVideo e PlayList).

Sendo que a tabela de último vídeo terá sempre somente os dados do último vídeo entregue ao aplicativo.

Note que o retorno de get() tem que ter a possibilidade de retorno null (LastVideo?), pois o código da Room API é construído em Java e caso não haja nenhum dado na tabela... null será retornado.

DAO PlayList

Ainda no pacote /data/dynamic crie a Interface DAO de PlayLists, PlayListDao, como a seguir:

package thiengo.com.br.canalvinciusthiengo.data.dynamic

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import thiengo.com.br.canalvinciusthiengo.model.PlayList

/*
* Interface de configuração de acesso à tabela
* PlayList da persistência local, Room API.
* */
@Dao
interface PlayListDao {

/**
* Salva na base local todas as PlayLists do
* canal obtidas até então.
*
* Caso tenha conflito de identificadores
* único (por ser a mesma PlayList), então
* salva os dados recém obtidos por cima dos
* já presentes em base. É esse tipo de
* estratégia que mantém o título da PlayList
* em dia com o que há disponível em canal
* no YouTube.
*
* @param playLists lista não mutável de
* PlayLists.
*/
@Insert( onConflict = OnConflictStrategy.REPLACE )
fun insertAll( playLists: List<PlayList> )

/**
* Retorna todas as PlayLists salvas em Room
* (SQLite) ou null caso não tenha ainda
* alguma [PlayList] em base.
*
* @return lista não mutável de PlayLists em
* base.
*/
@Query( value = "SELECT * FROM PlayList" )
fun getAll() : List<PlayList>?

/**
* Remove todos os dados [PlayList]
* presentes em base local. Isso deve ocorrer
* sempre antes de invocar o método
* insertAll() para garantir que somente
* o conjunto mais atual de PlayLists
* do canal permaneça no aplicativo.
*/
@Query( value = "DELETE FROM PlayList" )
fun deleteAll()
}

 

Aqui o método get() também tem que ter a sintaxe de possibilidade de retorno null (List<PlayList>?), pois é isso que ocorre se não houver nenhum dado da PlayList em banco de dados.

Dados iniciais em persistência

E, infelizmente, devido à limitação de consumo de dados diários da YouTube Data API...

... devido a isso é inteligente e altamente recomendado que mesmo as entidades que são preenchidas com dados externos (LastVideo e PlayList), mesmo essas tenham também as suas versões de dados estáticos.

Isso, para que o usuário seguidor do canal sempre tenha algo do canal em app enquanto um novo conteúdo não é liberado (entregue ao aplicativo).

Dados fixos LastVideo

No pacote /data/fixed crie a classe persistência LastVideoData com o exato código a seguir:

package thiengo.com.br.canalvinciusthiengo.data.fixed

import thiengo.com.br.canalvinciusthiengo.model.LastVideo

/**
* Contém o vídeo inicial que deve ser carregado
* junto ao aplicativo enquanto um vídeo mais
* atual não é enviado (ou acessado) a ele.
*
* O objetivo desta classe é trabalhar como uma
* persistência local estática, fixa, que contém
* os dados de algum vídeo do canal YouTube do
* app. Assim o usuário sempre terá algum
* "último" vídeo disponível para acesso, mesmo
* quando ainda não foi retornado (ou acessado)
* os dados do vídeo mais atual já disponível
* no canal.
*/
class LastVideoData {

companion object{
/**
* Retorna o "último" vídeo disponível
* por padrão no aplicativo
*
* @return objeto do tipo [LastVideo].
*/
fun getInitialVideo()
= LastVideo(
uid = "g8h8QkSPMQE",
title = "[MINI-CURSO] Porque e Como Utilizar " +
"Vetores no Android - PARTE 5",
description = "O vídeo acima é a quinta vídeo aula " +
"de um mini-curso completo sobre Drawables " +
"Vetoriais no desenvolvimento de aplicativos " +
"Android."
).apply {
thumbUrl = ""
}
}
}

 

Os dados de LastVideo presentes nesta classe são do último vídeo liberado no canal YouTube Thiengo até a época da construção deste artigo.

Note que para nossa lógica de negócio: o canal tem que ter ao menos um vídeo disponível.

Outro ponto a se notar é que thumbUrl na verdade não terá como valor um dado vazio, "". Isso devido a configuração set() que colocamos para ele em LastVideo:

...
set( value ) {
field = if( value.isNotEmpty() ){
value
}
else{
alternativeThumbUrl()
}
}
...

Dados fixos PlayList

Ainda no pacote /data/fixed crie a classe persistência PlayListsData com a seguinte definição:

package thiengo.com.br.canalvinciusthiengo.data.fixed

import thiengo.com.br.canalvinciusthiengo.model.PlayList

/**
* Contém as principais PlayLists vinculadas ao
* canal YouTube do aplicativo.
*
* O objetivo desta classe é trabalhar como uma
* persistência local estática, fixa, que contém
* os dados das principais PlayLists do canal.
*
* Pois devido às limitações da YouTube Data API
* é importante ter também em app os dados das
* principais PlayLists e assim ter certeza que
* o usuário terá acesso a elas.
*/
class PlayListsData {

companion object{
/**
* Retorna as principais PlayLists
* vinculadas ao canal.
*
* @return lista mutável de objetos
* [PlayList].
*/
fun getInitialPlayLists()
= mutableListOf(
PlayList(
title = "[MINI-CURSO] Porque e Como " +
"Utilizar Vetores no Android",
uid = "PLBA57K2L2RIJeKoaLgTtYKSFAhvH6FAcG"
),
PlayList(
title = "Como Desenvolver a Tela de " +
"Listagem de Calçados - Android M-Commerce",
uid = "PLBA57K2L2RII2XZc79MqnqeuhG6VqfYjM"
),
PlayList(
title = "Como Melhorar a Área de " +
"Configurações de Conta - Android " +
"M-Commerce",
uid = "PLBA57K2L2RIKwFMT9IU06wgFDlMW6WHJo"
),
PlayList(
title = "Como Desenvolver as Telas de " +
"Endereço de Entrega - Android " +
"M-Commerce",
uid = "PLBA57K2L2RIJ7uLasfzwBGiip4fcVH1oS"
),
PlayList(
title = "Desenvolvendo as Telas de Cartão " +
"de Crédito Com Máscara de Campo " +
"- Android M-Commerce",
uid = "PLBA57K2L2RILKKBEGsUk039no5sakLVOS"
),
PlayList(
title = "Como Desenvolver as Telas de " +
"Configuração de E-mail e Senha " +
"- Android M-Commerce",
uid = "PLBA57K2L2RIKHvU3LxgnSIzFxYZ5wtR2W"
)
)
}
}

 

Confesso que não coloquei todas as PlayLists do canal YouTube Thiengo, principalmente para economizar espaço aqui.

Mas em caso de cliente você deve colocar todas as PlayLists do canal dele disponíveis no tempo da criação do projeto.

Se não houver ao menos uma PlayList no canal do cliente, então retorne um lista mutável vazia: mutableListOf<PlayList>().

Você deve estar se questionando o porquê do retorno em getInitialPlayLists() ser uma lista mutável.

Já lhe adianto que isso é necessário devido à lógica de negócio que teremos no fragmento de PlayLists.

Quando chegarmos a esse ponto do projeto ficará claro o porquê da lista retornada em getInitialPlayLists() ter que ser mutável.

Até lá, não se preocupe, apenas siga construindo o projeto passo a passo como nos artigos.

InitialDataCallback

Agora sim podemos ir à classe que vai preencher o banco de dados com os dados em "classes persistência" que criamos anteriormente.

No pacote /data/dynamic crie a classe InitialDataCallback com a seguinte configuração de inicialização de banco de dados local:

package thiengo.com.br.canalvinciusthiengo.data.dynamic

import android.content.Context
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import thiengo.com.br.canalvinciusthiengo.data.fixed.LastVideoData
import thiengo.com.br.canalvinciusthiengo.data.fixed.PlayListsData

/**
* Classe responsável por inicializar a base de
* dados local com dados de "último vídeo" e
* principais PlayLists disponíveis.
*
* Desta forma o usuário seguidor do canal não
* corre o risco de ter em mãos um aplicativo
* "inútil" caso a cota de acesso do app ao
* YouTube Data API já tenha estourado.
*
* @property context contexto do aplicativo.
* @constructor cria um objeto completo do tipo
* [InitialDataCallback].
*/
class InitialDataCallback(
private val context: Context ) : RoomDatabase.Callback() {

override fun onCreate( db: SupportSQLiteDatabase ) {
super.onCreate( db )
initLastVideo()
initPlayLists()
}

/**
* Inicializa a tabela LastVideo do banco de
* dados local.
*/
private fun initLastVideo(){
/* TODO */
}

/**
* Inicializa a tabela PlayList do banco de
* dados local.
*/
private fun initPlayLists(){
/* TODO */
}
}

 

Ainda precisamos de alguns códigos em projeto para realmente podermos inicializar os dados em base local, por isso initLastVideo()initPlayLists() estão vazios.

Estes métodos na verdade são tratados como métodos comuns, então os códigos aos quais eles dependem são os mesmos códigos que qualquer outro ponto do projeto que depende de banco de dados local necessitaria.

Na próxima seção vamos a criação da entidade que também será necessária a todo o código de InitialDataCallback.

Entidade de banco de dados

Agora vamos a classe que realmente representa o banco de dados local Room.

No pacote /data/dynamic crie a classe ChannelDatabase com o código a seguir:

package thiengo.com.br.canalvinciusthiengo.data.dynamic

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import thiengo.com.br.canalvinciusthiengo.model.LastVideo
import thiengo.com.br.canalvinciusthiengo.model.PlayList

/**
* Classe de banco de dados local. Com todas as
* configurações necessárias para atender às
* necessidades de projeto.
*
* Para que qualquer atualização em estrutura
* de banco de dados local tenha efeito é
* preciso mudar a versão da base em "version"
* na anotação @Database.
*/
@Database(
entities = arrayOf(
LastVideo::class,
PlayList::class
),
version = 18
)
abstract class ChannelDatabase: RoomDatabase() {

companion object{
/**
* Constante com o nome do banco de
* dados local.
*/
private const val DB_NAME = "youtube-channel"

/**
* Constante responsável por conter a
* única instância de [ChannelDatabase]
* disponível durante toda a execução
* de cada chamada a banco de dados
* local.
*/
private var instance: ChannelDatabase? = null

/**
* Método que aplica, junto à propriedade
* instance, o padrão Singleton em classe.
* Garantindo que somente uma instância de
* [ChannelDatabase] estará disponível durante
* toda a execução de cada chamada a banco
* de dados local. Ajudando também a
* diminuir a possibilidade de vazamento
* de memória e de locks de tabela.
*
* @param context contexto do aplicativo.
* @return instância única de [ChannelDatabase].
*/
fun getInstance( context: Context ) : ChannelDatabase {

if( instance == null || !instance!!.isOpen ){

instance = Room.databaseBuilder(
context,
ChannelDatabase::class.java,
DB_NAME
)
.addCallback(
InitialDataCallback(
context = context
)
)
.fallbackToDestructiveMigration()
.build()
}
return instance!!
}
}

abstract fun lastVideoDao() : LastVideoDao
abstract fun playListDao() : PlayListDao
}

 

O uso do Singleton é opcional. Coloquei ele aqui, pois no final das contas vai nos ajudar a diminuir as chances de vazamento de memória.

Um ponto que vale ressaltar a explicação (além dos comentários já presentes em código) é o da anotação @Database:

...
@Database(
entities = arrayOf(
LastVideo::class,
PlayList::class
),
version = 18
)
...

 

É bem intuitivo que essa anotação indica que a classe que a contém é a classe que representa o banco de dados local Room do app.

Porém os dois parâmetros utilizados aqui podem não ser tão óbvios.

O primeiro, entities:

Contém um array dos objetos Class de cada uma das classes em projeto que têm a definição @Entity nelas.

O segundo, version:

Contém a versão atual do banco de dados. É importante entender que qualquer modificação em alguma @Entity ou alguma @Dao, para ser refletida na base de dados, precisará também de uma modificação em version.

Ou seja, mudou algo que afeta o banco de dados, então acrescente +1 ao valor definido atualmente em version.

Caso contrário a modificação não será refletida em projeto.

Outro ponto que é pouco intuitivo é a invocação do método fallbackToDestructiveMigration() na criação do banco de dados.

Esse método basicamente informa ao sistema que não precisa se preocupar com código de migração de dados se a versão do banco de dados tiver sido atualizada.

Apenas destrua tudo que existe de banco de dados até o momento e então crie um novo com a nova configuração.

Se esse método não fosse utilizado, seria necessário todo um código de migração de dados que é algo que não é importante em nosso domínio de problema.

E, por fim, em addCallback() temos então a nossa instância (ainda incompleta) de InitialDataCallback.

Com a adição da classe utilitária na próxima seção, nós vamos resolver o problema "incompleta" de InitialDataCallback.

Classe utilitária

Enfim a classe que vai facilitar consideravelmente a tarefa de salvar e obter dados do banco local em projeto.

Note que para este projeto, que para mim é sim um projeto pequeno em termos de código fonte. Aqui utilizaremos com frequência classes utilitárias (não somente a de banco de dados) que podem ser problemas em softwares mais complexos.

É isso.

No pacote /data/dynamic crie a classe UtilDatabase com o seguinte código:

package thiengo.com.br.canalvinciusthiengo.data.dynamic

import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import thiengo.com.br.canalvinciusthiengo.model.LastVideo
import thiengo.com.br.canalvinciusthiengo.model.PlayList
import kotlin.concurrent.thread

/**
* Classe utilitária que permite fácil acesso à
* base de dados local, Room API.
*
* Assim é possível obter de maneira imediata e
* não verbosa os métodos de inserção e obtenção
* de dados de: último vídeo; e PlayLists do canal.
*
* @property context contexto do aplicativo.
* @constructor cria um objeto completo do tipo
* [UtilDatabase].
*/
class UtilDatabase private constructor(
private val context: Context ){

companion object{
/**
* Propriedade responsável por conter a
* única instância de [UtilDatabase]
* disponível durante toda a execução do
* aplicativo.
*/
private var instance: UtilDatabase? = null

/**
* Método que aplica, junto à propriedade
* [instance], o padrão Singleton em classe.
* Garantindo que somente uma instância de
* [UtilDatabase] estará disponível durante
* toda a execução do app. Ajudando a
* diminuir a possibilidade de vazamento
* de memória.
*
* @param context contexto do aplicativo.
* @return instância única de [UtilDatabase].
*/
fun getInstance( context: Context ) : UtilDatabase {
if( instance == null ){
instance = UtilDatabase(
context = context
)
}
return instance!!
}
}

/**
* Retorna uma instância do banco de dados
* local para que seja possível manipular os
* dados nele.
*
* @return conexão com o banco de dados local.
*/
private fun getDatabase() : ChannelDatabase
= ChannelDatabase.getInstance( context = context )
}

 

Essa é apenas a parte inicial. Vamos aos trechos específicos de "último vídeo" e de PlayLists.

Note novamente em projeto o uso do operador force null (!!). Mais precisamente em:

...
return instance!!
...

 

Com segurança podemos fazer isso aqui, pois conhecemos o fluxo do código e sabemos que neste ponto da classe a propriedade instance nunca será null.

Antes, um detalhe importante:

A comunicação com o banco dados local tem que ser fora da Thread Principal.

Já estou sinalizando esse ponto aqui para você não se surpreender com o uso de thread{} nos métodos que estaremos adicionando em UtilDatabase.

Métodos para último vídeo

Na classe utilitária UtilDatabase adicione os métodos saveLastVideo() e getLastVideo() exatamente como a seguir:

...
/**
* Salva em banco de dados local os dados do
* último vídeo do canal obtido pelos
* algoritmos do app.
*
* Note que qualquer acesso ao banco de dados
* local via Room API deve ser fora da Thread
* Principal, por isso a necessidade de
* thread{} no código do método a seguir.
*
* @param lastVideo último vídeo disponível
* no canal.
*/
fun saveLastVideo( lastVideo: LastVideo ){
thread{
try {
val dataBase = getDatabase()

/**
* Garantindo que sempre terá
* somente um vídeo na tabela
* de vídeos.
*/
dataBase
.lastVideoDao()
.delete()

dataBase
.lastVideoDao()
.insert(
lastVideo = lastVideo
)

dataBase.close()
}
catch( e :Exception ){}
}
}

/**
* Retorna, via callback, o último vídeo do
* canal salvo em persistência local ou null
* caso não haja algum.
*
* Note que qualquer acesso ao banco de dados
* local via Room API deve ser fora da Thread
* Principal, por isso a necessidade de
* thread{} no código do método a seguir.
*
* @param callback função que vai trabalhar o
* retorno de [dataBase].lastVideoDao().get().
*/
fun getLastVideo(
callback: (LastVideo?)->Unit ){

thread {
try {
val dataBase = getDatabase()
val lastVideo = dataBase.lastVideoDao().get()
dataBase.close()

callback( lastVideo )
}
catch( e :Exception ){}
}
}
...

 

Note que esses métodos, e os de PlayLists desta classe utilitária, entram na classe fora do escopo de companion object.

Outro ponto importante:

Não vamos tratar as exceções de banco de dados que podem ocorrer.

Por isso o catch() vazio.

Isso, pois nós conhecemos todo o fluxo do projeto e um possível erro na recuperação dos dados locais não afetará em nada o funcionamento do app.

Tendo mente que as classes de persistência estática estarão sempre sendo utilizadas em paralelo à base de dados local dinâmica.

O trabalho com callback para obter o retorno do banco de dados se faz necessário, pois a base precisa ser acessada em Thread separada da Thread Principal.

Métodos para PlayLists

Ainda em UtilDatabase adicione os métodos savePlayLists() e getAllPlayLists() com o código fonte a seguir:

...
/**
* Salva em banco de dados local os dados das
* PlayLists do canal obtidas pelos algoritmos
* do app.
*
* Note que qualquer acesso ao banco de dados
* local via Room API deve ser fora da Thread
* Principal, por isso a necessidade de
* thread{} no código do método a seguir.
*
* @param playLists PlayLists disponíveis no
* canal.
*/
fun savePlayLists(
playLists: List<PlayList> ){

thread{
try {
val dataBase = getDatabase()

/**
* Garantindo que sempre terá em
* banco de dados local somente
* as PlayLists ainda ativas no
* canal YouTube do aplicativo.
*/
dataBase
.playListDao()
.deleteAll()

dataBase
.playListDao()
.insertAll(
playLists = playLists
)

dataBase.close()
}
catch( e :Exception ){}
}
}

/**
* Retorna, via callback, todas as PlayLists
* do canal salvas em persistência local ou
* null caso não haja alguma.
*
* Note que qualquer acesso ao banco de dados
* local via Room API deve ser fora da Thread
* Principal, por isso a necessidade de
* thread{} no código do método a seguir.
*
* @param callback função que vai trabalhar o
* retorno de [dataBase].playListDao().getAll().
*/
fun getAllPlayLists(
callback: (List<PlayList>?)->Unit ){

thread {
try {
val dataBase = getDatabase()
val playLists = dataBase.playListDao().getAll()
dataBase.close()

callback( playLists )
}
catch( e :Exception ){}
}
}
...

 

Agora somente falta atualizar os métodos initLastVideo()initPlayLists() de InitialDataCallback.

Atualizando InitialDataCallback

Com a classe utilitária criada, podemos ir à classe que é responsável por inicializar dados em base local.

Mais precisamente a classe InitialDataCallback.

Atualize os métodos initLastVideo() e initPlayLists() exatamente como a seguir:

...
private fun initLastVideo(){
UtilDatabase
.getInstance( context = context )
.saveLastVideo(
lastVideo = LastVideoData.getInitialVideo()
)
}

...
private fun initPlayLists(){
UtilDatabase
.getInstance( context = context )
.savePlayLists(
playLists = PlayListsData.getInitialPlayLists()
)
}
...

 

Bom, é isso.

Ao menos na classe utilitária nós certamente voltaremos para mais algumas atualizações necessárias em projeto.

Até este ponto do projeto temos a seguinte configuração física (das entidades de código dinâmico) em IDE:

Configuração física do projeto Android

Próximo conteúdo

Mais um importante trecho de projeto finalizado: banco de dados local via Room API.

No próximo conteúdo vamos a construção do fragmento de "Último Vídeo" liberado em canal. O principal fragmento do app.

Segue o link para acesso ao próximo conteúdo:

➙ Construindo a Tela e a Lógica de Último Vídeo - Parte 7.

Então é isso.

Relaxe um pouco. Veja um filme 🎞, tome um café ☕ 🍳. E...

... te vejo na Parte 7 do projeto.

Se houverem dúvidas ou dicas deste sexto conteúdo do aplicativo, então deixe nos comentários que logo eu lhe respondo.

Não esqueça de conhecer também o meu canal no YouTube (caso você ainda não conheça) e...

... não deixe de se inscrever na 📩 lista de e-mails para também garantir a versão em PDF não somente deste projeto de aplicativo Android, mas também de cada novo "conteúdo mini-curso".

Abraço.

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Lottie API Para Animações no AndroidLottie API Para Animações no AndroidAndroid
Annotation Span Para Estilização de Texto no AndroidAnnotation Span Para Estilização de Texto no AndroidAndroid
Políticas de Privacidade e Porque não a GDPR - Android M-CommercePolíticas de Privacidade e Porque não a GDPR - Android M-CommerceAndroid
Porque e Como Utilizar Vetores no AndroidPorque e Como Utilizar Vetores no AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...