Configurando o WorkManager Para Requisições em Background - YouTuber Android App - Parte 12

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 /Configurando o WorkManager Para Requisições em Background - YouTuber Android App - Parte 12

Configurando o WorkManager Para Requisições em Background - YouTuber Android App - Parte 12

Vinícius Thiengo
(3212)
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 12 do aplicativo nós vamos às configurações estratégicas das requisições remotas.

Requisições via classe utilitária de conexão com a YouTube Data API com auxílio também da WorkManager API.

Animação com atualização de último vídeo disponível em canal

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 12º 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.

Requisições em background

Com os algoritmos de conexão remota já configurados em projeto, podemos agora coloca-los nos locais estratégicos do aplicativo:

  • Nos dois fragmentos que também consomem dados remotos, LastVideoFragment e PlayListsFragment;
  • e Em execuções em background, quando o aparelho nem mesmo está em pilha de aplicativos em memória.

Vamos iniciar com as configurações em background, utilizando a WorkManager API para as requisições em janela de execução do app.

Por que a WorkManager API?

Na verdade a pergunta deveria ser: Por que não?

Se você é um desenvolvedor Android que já teve que garantir execuções em background, então certamente você tem conhecimento que a partir do Android 6, Marshmallow, as coisas ficaram bem difíceis para aqueles domínios de problema que dependiam de execuções em background.

Principalmente domínios que necessitavam de execuções com exatidão em intervalos fixos. De 5 em 5 minutos, por exemplo.

E o problema fica ainda mais crítico quando o aplicativo precisa atender a versões do Android abaixo da API 21, Lollipop.

Pois o JobScheduler não pode ser utilizado e sim um merge de BroadcastReceiver com AlarmManager.

Com o WorkManager, finalmente a configuração de trabalho em background ficou facilitada.

Pois a implantação é simples e única. Não é preciso criar hack codes que utilizam diferentes versões de algoritmos de acordo com a versão do Android em uso.

O uso das diferentes versões de APIs ainda existe, mas é tudo delegado para o WorkManager, ele resolve isso por debaixo dos panos, de maneira encapsulada.

Nós desenvolvedores somente temos que nos preocupar em configurar o WorkManager para atender as necessidades de nosso domínio de problema.

Por fim, como resumo:

Se seu aplicativo precisa de execuções em background, porém sem exatidão nos intervalos para execuções (exatamente o que precisamos aqui).

Então sem sombra de dúvidas a WorkManager é a melhor opção para você.

É isso. Podemos ir aos códigos.

Configuração no Gradle

No Gradle Nível de Aplicativo, ou build.gradle (Module: app), adicione ao final do bloco android as configurações de versão Java em compileOptions e em kotlinOptions como a seguir:

...
android {
...

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
...

 

Sim, o Java 8 ou superior é necessário para que a WorkManager API possa ser utilizada em projeto.

Ainda no Gradle Nível de Aplicativo, agora no bloco dependencies, adicione a dependência WorkManager como a seguir:

...
dependencies {
...

/*
* Para uso da WorkManager API. API responsável por
* trabalhos em background quando o app não está em uso.
* */
implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'androidx.core:core-ktx:1.3.1'
}
...

 

Por fim, sincronize o projeto.

Criando a classe Worker

Agora podemos criar a classe Worker responsável por algumas invocações em background utilizando a classe abstrata UtilNetwork.

A classe Worker é simples, mas teremos que construir uma "corrente" correta de como as invocações em background devem ocorrer.

Assim, no pacote /network, crie um novo pacote de rótulo /worker.

Agora, neste novo pacote, crie a classe CatchChannelDataWorker com o exato código inicial a seguir:

package thiengo.com.br.canalvinciusthiengo.network.worker

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import thiengo.com.br.canalvinciusthiengo.data.dynamic.UtilDatabase
import thiengo.com.br.canalvinciusthiengo.model.LastVideo
import thiengo.com.br.canalvinciusthiengo.network.NetworkRequestMode
import thiengo.com.br.canalvinciusthiengo.network.UtilNetwork
import thiengo.com.br.canalvinciusthiengo.notification.UtilNotification

/**
* Classe Worker de trabalho periódico em background,
* mesmo quando o aplicativo não está em memória.
*
* Com o objeto desta classe Worker é possível manter
* os dados de "último vídeo" e de PlayLists do canal
* atualizados em app.
*
* @property context contexto do aplicativo.
* @property params parâmetros de trabalho do
* WorkManager.
* @constructor cria um objeto completo do tipo
* CatchChannelDataWorker.
*/
class CatchChannelDataWorker(
private val context: Context,
params: WorkerParameters ) : Worker( context, params ) {

companion object{
/**
* Constantes com alguns dados da configuração
* inicial de WorkManager.
*/
const val NAME = "sync_local_database"
const val REPEAT_INTERVAL : Long = 18
}

override fun doWork(): Result {

/* TODO */

return Result.success()
}
}

 

Neste ponto é prudente passarmos primeiro pelos significados das constantes:

NAME:

O nome que identifica essa configuração de Worker no sistema. Assim é possível atualiza-lo ou até mesmo remove-lo do agendamento de execução.

REPEAT_INTERVAL:

Um valor Long com o intervalo entre uma execução e outra em background.

Em nosso caso o 18 significará "execução a cada 18 horas".

Na definição do código de configuração de WorkManager em sistema (que ficará em nossa classe CustomApplication) nós teremos a definição do tipo de intervalo (TimeUnit.HOURS).

O intervalo mínimo possível na API é de 15 minutos.

Note que a definição do intervalo é apenas um "desejo". O sistema Android tentará ao máximo manter a execução na janela de intervalo correta.

Porém... há modelos Android que a execução do WorkManager é realmente prejudicada, ou seja, o intervalo entre uma execução e outra pode ser muito maior do que o definido em configuração.

Mas é aquilo, nós como desenvolvedores desse aplicativo para YouTuber podemos ficar tranquilos.

Pois todo o código dependente dos algoritmos de comunicação remota (em /network) são códigos de uma redundância com não muita importância.

Tendo em mente que a YouTube Data API nos limita a poucos dados diários por chave de API e que...

... a notificação OneSignal já é responsável, e não limitada, em entregar o principal dado ao app: último vídeo disponível em canal.

Note que eu coloquei 18 horas, pois este é um intervalo completamente aceitável no canal Vinícius Thiengo de acordo com a frequência de novas postagens nele.

Mas se você for atender um cliente YouTuber que coloca, por exemplo, seis vídeos por dia. Então coloque um intervalo de 4 horas ou até menos... na casa dos minutos.

Explicadas as constantes, ainda temos o método doWork(). Este que é invocado nas execuções da classe Worker. Execuções que ocorrem fora da Thread Principal.

Sendo assim é seguro e prudente realizar as invocações a UtilNetwork com a definição NetworkRequestMode.SYNCHRONOUS.

O retorno Result.success() em doWork() é porque independente do resultado a partir de doWork(), em nosso domínio de problema, somente de ter entrado neste método já podemos considerar que é algo bem sucedido.

Como será o roteiro?

O código que temos que colocar a partir de doWork(), para o nosso domínio de problema, deverá seguir o roteiro:

  1. Primeiro solicitar ao YouTube os dados de PlayLists disponíveis no canal;
  2. Ao final da execução, obter do banco de dados local o "último vídeo" salvo em base;
  3. Com o último vídeo salvo em base "em mãos", podemos então solicitar ao YouTube os dados de último vídeo disponibilizado em canal;
  4. Se o vídeo retornado pelo YouTube for diferente do vídeo salvo em canal, então além dele ser salvo na base local nós teremos que gerar uma notificação na bandeja de notificações.

Note que algumas regras de negócio acima já acontecerão sem o código em CatchChannelDataWorker precisar ter isso de maneira explícita.

Por exemplo:

"A notificação somente pode ser gerada se a atividade não estiver em foreground."

Ok. O código em UtilNotification, mais precisamente em createBigPictureNotification(), já faz essa verificação internamente.

"Se o vídeo retornado pelo YouTube for diferente do vídeo salvo em base local, então o vídeo retornado deverá ser salvo no lugar do atual."

Ok. O código de UtilDatabase, mais precisamente em saveLastVideo(), que na verdade é invocado devido a invocação de retrieveLastVideo() de UtilNetwork... esse código já faz isso.

Inclusive atualiza o fragmento LastVideoFragment se o aplicativo estiver em foreground.

Com isso podemos ir aos novos códigos de CatchChannelDataWorker.

Vídeo novo ou não?

Vamos primeiro adicionar o método que é responsável por verificar se o vídeo é um novo vídeo ou não e então, caso sim, também gera uma notificação.

Isso tudo respeitando as regras de negócio do aplicativo já discutidas anteriormente.

Em CatchChannelDataWorker adicione o método retrieveServerLastVideo() como a seguir:

...
/**
* Obtém, do YouTube Data API (servidor remoto) e por
* meio de callback, o "último vídeo" disponível em
* canal YouTube vinculado ao app.
*
* @param oldLastVideo "último vídeo", obtido do banco
* de dados local.
*/
private fun retrieveServerLastVideo(
oldLastVideo: LastVideo? ){

UtilNetwork
.getInstance( context = context )
.retrieveLastVideo(
networkRequestMode = NetworkRequestMode.SYNCHRONOUS,
callbackSuccess = {

/**
* Somente cria uma nova notificação se o
* último vídeo liberado no canal não está
* ainda salvo no banco de dados local.
*/
if( oldLastVideo == null
|| !oldLastVideo.uid.equals( it.uid )
|| !oldLastVideo.title.equals( it.title )
|| !oldLastVideo.title.equals( it.description )){

UtilNotification
.getInstance( context = context )
.createBigPictureNotification( lastVideo = it )
}
}
)
}
...

 

Note que para o vídeo recém obtido do YouTube ser um "último novo vídeo" aceito em aplicativo é preciso que apenas um dos dados do vídeo obtido do YouTube seja diferente do vídeo já salvo em base local:

...
if( oldLastVideo == null
|| !oldLastVideo.uid.equals( it.uid )
|| !oldLastVideo.title.equals( it.title )
|| !oldLastVideo.title.equals( it.description ) ){
...

 

Lembrando que os imports necessários já estavam todos em classe.

Outro ponto é que este novo método é invocado a partir do retorno do banco de dados local sobre a solicitação: me retorne o "último vídeo" disponível em base.

Devolva o que já existe em banco

Agora vamos ao método responsável por acessar o "último vídeo" disponível na base local do app para assim ser possível realizar uma comparação com o que foi retornado pelo YouTube.

Ainda em CatchChannelDataWorker adicione o método retrieveLocalLastVideo() a seguir:

...
/**
* Obtém, por meio de callback, o "último vídeo" já
* salvo em banco de dados local.
*/
private fun retrieveLocalLastVideo(){
UtilDatabase
.getInstance( context = context )
.getLastVideo{
retrieveServerLastVideo( oldLastVideo = it )
}
}
...

 

Com isso podemos ir à configuração final em CatchChannelDataWorker, no método doWork().

Obtendo as PlayLists

Em doWork() nós vamos colocar o código que solicita as PlayLists do canal.

E independente de ter PlayList ou não no canal, se não houver problemas na conexão então será um retorno bem sucedido.

Sendo assim, em doWork() adicione o código a seguir:

...
override fun doWork(): Result {
UtilNetwork
.getInstance( context = context )
.retrievePlayLists(
networkRequestMode = NetworkRequestMode.SYNCHRONOUS,
callbackSuccess = {
retrieveLocalLastVideo()
}
)

return Result.success()
}
...

 

Novamente, como doWork() já executa em uma Thread secundária, seguramente podemos informar ao Retrofit que a comunicação com os servidores do YouTube podem ser na mesma Thread atual (NetworkRequestMode.SYNCHRONOUS).

Ou seja, requisição síncrona.

Com isso podemos ir ao código de inicialização de nosso Worker.

Iniciando o objeto Worker

Em nossa Application personalizada, CustomApplication, adicione o método backgroundWork() como a seguir:

...
/**
* Inicializa o WorkManager para execuções
* não precisas, mas intervaladas, em
* background.
*/
private fun backgroundWork(){
val request = PeriodicWorkRequestBuilder<CatchChannelDataWorker>(
CatchChannelDataWorker.REPEAT_INTERVAL,
TimeUnit.HOURS
).build()

/*
* Configuração de WorkManager que
* garante que mesmo com uma
* "re-invocação" de enfileiramento de
* "work" não haverá work repetido em
* lista de execução do WorkManager.
* */
WorkManager
.getInstance( this )
.enqueueUniquePeriodicWork(
CatchChannelDataWorker.NAME,
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
...

 

Estamos trabalhando com uma PeriodicWorkRequest, pois assim não precisamos, ao final de cada execução doWork(), criar um novo work em sistema.

Acredite... tem muitos apps que fazem isso devido ao desconhecimento de PeriodicWorkRequest.

A definição ExistingPeriodicWorkPolicy.KEEP em enqueueUniquePeriodicWork() faz com que não tenha problema algum a invocação do algoritmo em backgroundWork() mais de uma vez.

Pois a definição ExistingPeriodicWorkPolicy.KEEP mantém em sistema tudo que já foi definido sobre o Worker, exatamente como já estava, não há sobrescrita.

Agora podemos atualizar o onCreate() da CustomApplication como a seguir:

...
override fun onCreate() {
super.onCreate()

oneSignalInit()
backgroundWork()
}
...

 

Done.

Algoritmo de execução em background configurado como definido na parte estratégica do projeto.

Requisições de atualização

Ainda é preciso colocar as requisições aos servidores do YouTube nos fragmentos de "último vídeo" e de PlayLists.

Pois a partir desses, com a ação de swipe do usuário em tela, será possível atualizar o conteúdo:

Swipe refresh sendo aplicado no aplicativo Android para YouTubers

Em resumo, é isso que faremos nos fragmentos de último vídeo e de PlayLists:

  • Adicionar um swipe ao layout;
  • Com o acionamento do swipe, realizar a conexão com o YouTube via UtilNetwork.

Vamos iniciar com LastVideoFragment.

Requisição de LastVideo

Vamos começar pela parte simples, os códigos estáticos, logo depois vamos aos códigos dinâmicos em fragmento.

Layout

Com a necessidade de um listener de swipe, o layout /res/layout/fragmento_last_video.xml passa a ter a seguinte nova estrutura:

Diagrama do layout fragmento_last_video.xml

E a seguinte nova configuração XML:

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/srl_update_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragment.LastVideoFragment">

<LinearLayout
...>

...
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

 

Somente o SwipeRefreshLayout foi adicionado, nada foi removido. O SwipeRefreshLayout passou a ser o novo ViewGroup root.

Código dinâmico

Nosso primeiro passo em LastVideoFragment é adicionar um método que permite controlar o status do swipe em tela, pois essa mudança de status ocorrerá em mais de um ponto do fragmento.

Sendo assim, em LastVideoFragment, adicione o método swipeRefreshStatus() como a seguir:

...
/**
* Configura o estado atual do componente visual
* SwipeRefresh.
*
* @param status estado atual do swipe.
*/
private fun swipeRefreshStatus( status : Boolean ){
activity?.runOnUiThread {
srl_update_content?.isRefreshing = status
}
}
...

 

Com é possível que este novo método seja invocado em uma Thread secundária, então foi necessário adicionar a atualização de estado do componente visual dentro de runOnUiThread().

E, novamente, o uso massivo do operador not null (?.) é para garantir que não teremos um NullPointerException em mudanças rápidas de tela pelo menu principal do app.

Prosseguindo... em LastVideoFragment, adicione o import a seguir:

...
import thiengo.com.br.canalvinciusthiengo.network.UtilNetwork
...

 

Agora, no mesmo fragmento, adicione o método retrieveData():

...
/**
* Solicita dados de último vídeo da fonte remota,
* YouTube Data API.
*/
private fun retrieveData(){
UtilNetwork
.getInstance( context = activity!! )
.retrieveLastVideo(
callbackSuccess = {
swipeRefreshStatus( status = false )
setUiModel( lVideo = it )
},
callbackFailure = {
swipeRefreshStatus( status = false )
}
)
}
...

 

Novamente, vale ressaltar... o uso de Force NullPointerException (!!) em activity!! é porque nós conhecemos o fluxo do código e sabemos que neste ponto activity não será null.

Ainda em LastVideoFragment, adicione a configuração de listener de swipe no método setListeners():

...
private fun setListeners(){
...

srl_update_content?.setOnRefreshListener {
swipeRefreshStatus( status = true )
retrieveData()
}
}
...

 

Done.

Configuração de requisição remota a partir de LastVideoFragmet, com o swipe aplicado pelo usuário...

... essa configuração está finalizada.

Requisição de PlayLists

Como em "último vídeo", aqui em PlayLists vamos iniciar pela parte estática, XML.

Layout

Com a necessidade de inclusão de um SwipeRefreshLayout, a nova estrutura do layout /res/layout/fragment_play_lists.xml fica como a seguir:

Diagrama do layout fragment_play_lists.xml

E o XML deste layout fica assim:

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/srl_update_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragment.PlayListsFragment">

<LinearLayout
...>
...
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

 

Como no layout de LastVideoFragment, a única coisa que mudou foi a adição de SwipeRefreshLayout como o novo ViewGoup root.

Todo o restante, até mesmo o ViewGroup root anterior, permanece no layout como componente filho de SwipeRefreshLayout.

Código dinâmico

No código dinâmico de PlayListsFragment vamos também iniciar adicionando o método responsável por atualizar o estado do swipe em tela.

Adicione o método swipeRefreshStatus() ao fragmento como a seguir:

...
/**
* Configura o estado atual do componente visual
* SwipeRefresh.
*
* @param status estado atual do swipe.
*/
private fun swipeRefreshStatus( status : Boolean ){

activity?.runOnUiThread {
srl_update_content?.isRefreshing = status
}
}
...

 

Agora vamos à adição do import de UtilNetwork:

...
import thiengo.com.br.canalvinciusthiengo.network.UtilNetwork
...

 

Então, ainda no fragmento PlayListsFragment, adicione o método retrieveData() como a seguir:

...
/**
* Solicita dados de PlayLists da fonte remota,
* YouTube Data API.
*/
private fun retrieveData(){
UtilNetwork
.getInstance( context = activity!! )
.retrievePlayLists(
callbackSuccess = {
swipeRefreshStatus( status = false )
setUiModel( it )
},
callbackFailure = {
swipeRefreshStatus( status = false )
}
)
}
...

 

Agora temos que adicionar a configuração de listener de swipe:

...
/**
* Configura o listener de swipe do componente
* visual SwipeRefresh.
*/
private fun setListener(){
srl_update_content.setOnRefreshListener {
swipeRefreshStatus( status = true )
retrieveData()
}
}
...

 

Esse listener será configurado a partir do método onActivityCreated() de PlayListsFragment:

...
override fun onActivityCreated( ... ){
super.onActivityCreated( ... )

setListener()
...
}
...

 

E por fim, temos ainda que colocar a possibilidade de atualização de estado do SwipeRefresh dentro do método uiDataStatus().

Logo no início de runOnUiThread():

...
private fun uiDataStatus( ... ){

activity?.runOnUiThread {
swipeRefreshStatus( status = false )

...
}
}
...

 

Acredite: primeiro release finalizado.

Neste ponto final de nosso app, framework, Android temos a seguinte configuração física (pacotes das entidades de código dinâmico) em IDE:

Configuração física do projeto Android

Próximo conteúdo

Com os códigos de primeiro release finalizados, podemos ir com segurança para os testes e resultados.

Nosso próximo passo será testar todo o projeto de acordo com o que era esperado nessa primeira versão dele.

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

➙ Testes e Resultados no Projeto Finalizado - Parte 13.

Então é isso.

Relaxe um pouco 💆‍♂ 🎱. Tome um café ☕ 🥖 🍐. E...

... te vejo na Parte 13 do projeto.

Se houverem dúvidas ou dicas deste 12º 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

PhotoView Android Para a Completa Implementação de ZoomPhotoView Android Para a Completa Implementação de ZoomAndroid
Observable Binding Para Atualização na UI AndroidObservable Binding Para Atualização na UI AndroidAndroid
Live Templates Para Otimização de Tempo no Android StudioLive Templates Para Otimização de Tempo no Android StudioAndroid
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...