Configurando a YouTube Data API Com a Biblioteca Retrofit - YouTuber Android App - Parte 11

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 a YouTube Data API Com a Biblioteca Retrofit - YouTuber Android App - Parte 11

Configurando a YouTube Data API Com a Biblioteca Retrofit - YouTuber Android App - Parte 11

Vinícius Thiengo
(160)
Go-ahead
"É preciso encarar todo retrocesso, fracasso ou dificuldade como provações ao longo do caminho, como sementes plantadas para a colheita futura. Nenhum momento é desperdiçado se você presta atenção nas lições contidas em cada experiência."
Robert Greene
Kotlin Android
Capa do livro Mapas Android de Alta Qualidade - Masterização Android
TítuloMapas Android de Alta Qualidade - Masterização Android
CategoriasAndroid, Kotlin, Masterização, Especialização
AutorVinícius Thiengo
Edição
Ano2020
Capítulos11
Páginas166
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
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 onze do conteúdo vamos à configuração completa do algoritmo que consumirá os dados de "último vídeo" e de PlayLists do canal YouTube do app.

Algoritmo que consumirá os dados direto dos servidores de dados compartilhados do YouTube.

App Android consumindo dados da YouTibe Data API via Retrofit API

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 nono 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.

O porquê da YouTube Data API

O Google YouTube permite que desenvolvedores possam consumir dados do YouTube de maneira simples, JSON, por meio da YouTube Data API.

A API backend é REST, mas a API que utilizaremos em código Android é de nossa escolha.

A real vantagem do trabalho com a YouTube Data API é que para consumir os dados (request), nós não precisamos ter autoridade de administrador do canal, basta utilizar o ID do canal e prosseguir com o consumo.

Obviamente que para enviar dados ao canal, como um novo vídeo. Ai sim será preciso utilizar as chaves de API vinculadas à conta Gmail do canal.

Mas em nosso domínio de problema somente trabalharemos com dados consumidos, mais precisamente os dados de:

  • Último vídeo liberado em canal;
  • PlayLists vinculadas ao canal.

Redundância para melhorar a eficácia

Será justamente o consumo de dados da YouTube Data API que será a "parte fraca" de nossa redundância em consumo de novos dados do canal vinculado ao aplicativo.

Digo redundância fraca, pois informar ao usuário do app sobre um novo vídeo por meio do Dashboard OneSignal será muito mais eficaz.

Isso, pois há uma única limitação na YouTube Data API que coloca o projeto de aplicativo totalmente em cheque se ele depender somente desta API de dados para "sobreviver".

Quotas de requisição

A YouTube V3 API (aqui V3 API será utilizada como sinônimo de Data API) tem uma limitação de requisições diárias por chave de API e não por chave de usuário único de seu aplicativo.

E esse limite é bem pequeno, pois ainda em ambiente de teste, com algumas dezenas de requisições, o limite é atingido e a seguinte mensagem passa (objeto JSON) a ser retornada em arquivo JSON de resposta da YouTube Data API:

{
"error": {
"code": 403,
"message": "The request cannot be completed because you have exceeded your <a href=\"/youtube/v3/getting-started#quota\">quota</a>.",
"errors": [
{
"message": "The request cannot be completed because you have exceeded your <a href=\"/youtube/v3/getting-started#quota\">quota</a>.",
"domain": "youtube.quota",
"reason": "quotaExceeded"
}
]
}
}

 

Na comunidade Android há reclamações de usuários, que são desenvolvedores e consomem a YouTube V3 API, informando que mesmo quando aumentando os limites de cobrança deles no Console do Google...

... que mesmo assim o limite de quotas de consumo da YouTube Data API não aumentou de maneira aceitável.

Tendo em mente que para aumentar o limite é preciso entrar em contato com o Google, via Console, solicitando uma quota ainda maior de dados diários:

Console Google Dev para quotas diárias da YouTube Data API v3

E, a princípio, de acordo com os possíveis limites mais altos que podem ser disponibilizados para uma chave de API...

... esses limites mais altos são ainda pequenos se o canal tiver algumas dezenas de milhares de usuários que venham a instalar o aplicativo Android.

Resumo:

É inteligente utilizar a YouTube V3 API somente como uma redundância fraca (também em background) quando o assunto é "obter dados do YouTube".

E eu até entendo a limitação.

Pois caso contrário o aplicativo do YouTube teria inúmeros apps concorrentes que basicamente consumiriam todos os dados da YouTube Data API.

Outro ponto, caso você tenha pensado isso, não tente criar um algoritmo que consuma os dados do canal utilizando a popular JSOUP.

Isso certamente no médio longo prazo será falho. Pois o algoritmo dependerá única e exclusivamente da estrutura HTML das páginas Web do YouTube.

Sendo assim, aqui já é possível entender o porquê de termos toda uma configuração de dados iniciais em InitialDataCallback também e de termos colocados tantos detalhes e atenção no algoritmo de conteúdo vindo do OneSignal.

Agora vamos a configuração em projeto desse algoritmo de requisição redundante. Pois mesmo com a limitação em quotas de requisição, ele ainda é útil...

... porque vai que "da noite para o dia" essas quotas sejam aumentadas ou até mesmo extintas! Nosso aplicativo já estará pronto.

Geração de chave no Google Console

Antes de seguirmos com os códigos de consumo e trabalho com dados retornados da YouTube Data API... antes disso temos que primeiro gerar a chave de API e logo depois liberar nosso acesso aos dados da YouTube Data API.

Sendo assim, siga os passos:

  • Entre no Console de Credenciais do Google com o seu Gmail utilizado em projeto até este momento;
  • Pode ser que apareça uma tela de Termos e Condições de Uso logo no primeiro acesso. Apenas concorde e continue;
  • Já no Console de Credenciais clique em "+ CREATE CREDENTIALS";
  • Logo depois clique em "API key";

Criando uma nova chave de API no Google Dev Console

  • Aguarde à criação da API key até aparecer a janela com ela pronta;

Nova chave de API Google criada no Console de desenvolvedores

  • Copie a chave de API gerada.

Ainda não feche o Console do Google.

Vamos agora à classe abstrata de configuração YouTubeConfig.

Nesta classe adicione a chave de API gerada como a seguir, em uma nova constante dentro de uma nova classe abstrata interna:

...
abstract class YouTubeConfig {

abstract class Key {
companion object {
/**
* Constante com a chave de API do Google
* para que seja possível realizar consultas
* à YouTube Data API.
*/
const val GOOGLE_DEV = "AIzaSy..."
}
}
...
}

 

Ainda no Console do Google, vamos liberar o acesso à YouTube Data API para a chave de API gerada:

  • No menu lateral clique em "Library";

Acessando a área de bibliotecas Google no Console de desenvolvedores

  • Na próxima tela clique em "YouTube Data API v3";

Acessando a YouTube Data API v3

  • Clique em "Ativar" e aguarde;

Ativando a YouTube Data API v3

  • Quando o Dashboard de administração de consumo de dados aparecer... então está tudo ok para seguirmos agora no aplicativo.

YouTube Data API v3 ativada em Console

Com as configurações de chave finalizadas, podemos partir para os códigos.

Consumo de dados remotos

Como informado anteriormente:

A YouTube Data API (ou apenas API V3) não nos fornece uma API nativa Android para consumir os dados dos servidores do YouTube.

Sendo assim nós temos que escolher alguma API Android para consumo de dados remotos.

E se você já me acompanha a algum tempo deve saber que certamente eu vou optar pela Retrofit API.

Mas Thiengo, essa API é complicada de utilizar. São necessárias muitas configurações extras em projeto!

É um engano pensar isso... principalmente se você estiver preocupado também com a qualidade da conexão remota.

Por que a Retrofit?

Realmente há inúmeras APIs de comunicação remota que são um pouco mais simples de utilizar em projeto do que a Retrofit.

Porém a comunicação remota (conexão HTTP) é um tipo de recurso que temos que ter mais critérios, além de somente facilidade em configuração, para a seleção de qual API utilizar em projeto.

Recursos como:

  • A quanto tempo a API já está na comunidade Android;
  • Quais os clientes (aplicativos Android) já utilizam a API;
  • Qual a aceitação dela na comunidade (literalmente ➙ número de estrelas no GitHub);
  • Quando foi a última atualização, commit em repositório.

Depois de configurada a API de comunicação remota em projeto, muito provavelmente não mais mudaremos. Digo, não mudaremos de API ao menos no curto e médio prazo.

Eu confesso que a configuração de uso da Retrofit não é nem de perto uma das mais simples, mas para mim ela é ainda simples e, de qualquer forma, pelo resultado entregue vale o custo benefício.

Se essa for a primeira vez que você utiliza um recurso de comunicação remota no Android, saiba que junto aos conhecimentos de:

Junto a esses conhecimentos o domínio de "comunicação remota" é de extrema importância para qualquer desenvolvedor Android.

Digo, extrema importância depois que você já tem a base de conhecimento Android.

Bom, com isso podemos partir para os códigos.

Entidade de configuração da API V3

Nosso primeiro passo em código é "preparar o terreno".

Mais precisamente: já adicionar em YouTubeConfig as constantes que estaremos utilizando para conseguir realizar, via Retrofit, a comunicação com os servidores do YouTube.

Em YouTubeConfig adicione a classe abstrata ApiV3 com as seguintes constantes definidas:

abstract class YouTubeConfig {
...

abstract class ApiV3 {
companion object {
/**
* Constante com a URL base para acesso à
* YouTube Data API.
*/
const val BASE_URL = "https://www.googleapis.com/"

/**
* Constantes com os caminhos URL para
* acesso aos dados corretos (vídeo e
* PlayLists).
*/
const val VIDEO_PATH = "youtube/v3/search"
const val PLAYLISTS_PATH = "youtube/v3/playlists"

/**
* Constantes com as definições de
* parâmetros que devem estar junto a URL
* final de acesso a dados via YouTube Data
* API.
*/
const val PART_PARAM = "snippet"
const val MAX_RESULTS_VIDEO_PARAM = "1"
const val MAX_RESULTS_PLAYLISTS_PARAM = "500"
const val ORDER_PARAM = "date"
}
}
}

 

Todas as constantes definidas anteriormente ficarão claras em projeto quando chegar o momento de defini-las na API de comunicação remota que estaremos desenvolvendo.

Thiengo, espere. Estou confuso. Nós que criaremos a API? E o Retrofit?

Sim, criemos uma API para poder permitir que a Retrofit se comunique com o os servidores do YouTube.

Estaremos criando na verdade uma Interface para "guiar" a Retrofit na comunicação.

É exatamente está parte que alguns desenvolvedores reclamam:

A necessidade de criar uma estrutura, Interface, extra para poder realizar a comunicação.

Mas se você passa a enxergar a Retrofit também como um framework de comunicação remota. Então você passa a entender que o framework somente precisa de uma Interface de comunicação para tudo seguir como esperado.

E por causa desse framework, em um mesmo aplicativo é possível ainda realizar as mais diversas requisições remotas para diferentes servidores...

... sempre com o mesmo framework, mudando somente a Interface.

Então é isso, nossas novas constantes estão já definidas. Vamos agora ao Gradle Nível de Aplicativo.

Atualizando o Gradle

No Gradle Nível de Aplicativo, ou build.gradle(), adicione no bloco dependencies as configurações a seguir:

...
dependencies {
...

/*
* Retrofit API para comunicação remota. O Parse é
* realizado com a Gson API.
* */
def retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
}

 

Note que já adicionamos também a referência à biblioteca Gson (converter-gson). É ela que utilizaremos para aplicar o parse no JSON retornado dos servidores do YouTube.

Sincronize o projeto.

Novamente:

Se na época em que você estiver implementando este projeto houver uma versão mais atual da Retrofit, então utilize a versão mais atual, pois ela deverá funcionar sem problemas no projeto.

Classes parse

Já lhe adianto que essa é a parte do projeto que mais me "dói o coração" 😥.

Pois para esse primeiro release eu optei por aplicar o parse no JSON de retorno da YouTube Data API da maneira mais simples possível.

Ou seja, criando todo um conjunto de classes (são muitas - boilerplate) com as propriedades corretas para que o parse Gson seja aplicado de imediato, sem necessidade de configurações extras via annotations.

Então sim, a minha desculpa para isto, "todo um conjunto de classes (...) sem necessidade de configurações extras via annotations", é:

Esse é um primeiro release, não vamos perder tempo em melhorar essa parte agora. Essa tarefa é perfeitamente adiável.

Assim, vamos às classes parse.

Vídeo

O JSON retornado com as informações de último vídeo disponível em canal é o seguinte:

{
"kind": "youtube#searchListResponse",
"etag": "wUpM19Bsy_EKt68KzTDTsXEfo7I",
"nextPageToken": "CAEQAA",
"regionCode": "BR",
"pageInfo": {
"totalResults": 656,
"resultsPerPage": 1
},
"items": [
{
"kind": "youtube#searchResult",
"etag": "l3RgzndlGQbc64Bg4xqAGmzrzvU",
"id": {
"kind": "youtube#video",
"videoId": "g8h8QkSPMQE"
},
"snippet": {
"publishedAt": "2020-07-17T13:38:47Z",
"channelId": "UCG3gFuIkRF3PpNkRk3Wp6dw",
"title": "[MINI-CURSO] Porque e Como Utilizar Vetores no Android - PARTE 5",
"description": "android #vectorDrawable O vídeo acima é a quinta vídeo aula de um mini-curso completo sobre Drawables Vetoriais no desenvolvimento de aplicativos ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/g8h8QkSPMQE/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/g8h8QkSPMQE/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/g8h8QkSPMQE/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Vinícius Thiengo",
"liveBroadcastContent": "none",
"publishTime": "2020-07-17T13:38:47Z"
}
}
]
}

 

Por causa do JSON de resposta e porque estamos em um primeiro release de projeto... teremos, pasme, seis novas classes em pacote de classes de domínio.

Mas fique tranquilo, foi o que eu informei anteriormente: serão classes com códigos simples e boilerplate.

Com esse novo conjunto de classes parse o nosso objetivo será conseguir, ao final do parse, o fácil acesso aos seguintes dados de vídeo:

  • Identificador único ➙ videoObj.items[0].id.videoId;
  • Título ➙ videoObj.items[0].snippet.title;
  • Descrição (se houver alguma) ➙ videoObj.items[0].snippet.description;
  • e URL de thumb ➙ videoObj.items[0].snippet.thumbnails.high.url.

Então é isso.

Vamos começar a partir das classes com menos dependências e seguir para as com mais dependências.

No pacote /model crie o pacote /parse e dentro deste novo pacote crie o pacote /video.

Dentro de /model/parse/video crie a classe parse IdParse com o código a seguir:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém o identificador único do vídeo.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API) que
* é retornado pelo servidor do YouTube com os
* dados do último vídeo liberado no canal
* vinculado ao app.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property videoId identificador único do vídeo.
* @constructor cria um objeto completo do tipo
* [IdParse].
*/
class IdParse( val videoId: String )

 

No mesmo pacote crie a classe ThumbParse com o seguinte código:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém dados importantes ao app da thumb
* do último vídeo liberado no canal YouTube
* do aplicativo.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API)
* que é retornado pelo servidor do YouTube com
* os dados de último vídeo do canal vinculado ao
* app.
*
* Apesar de outros dados de thumb estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente a URL.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property url URL da thumb do vídeo.
* @constructor cria um objeto completo do tipo
* [ThumbParse].
*/
class ThumbParse( val url: String )

 

Ainda em /model/parse/video crie a classe ThumbnailParse com o fonte a seguir:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém dados da versão de mais alta resolução
* da thumb do último vídeo liberado no canal
* YouTube do aplicativo.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API)
* que é retornado pelo servidor do YouTube com
* os dados de último vídeo do canal vinculado ao
* app.
*
* Apesar de outros dados de thumbnail estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente o dado de versão de
* alta resolução da thumb.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property high thumb de mais alta resolução
* do vídeo.
* @constructor cria um objeto completo do tipo
* [ThumbnailParse].
*/
class ThumbnailParse( val high: ThumbParse )

 

Ainda no pacote /video adicione a classe SnippetParse com o código a seguir:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém dados importantes ao app do último vídeo
* liberado no canal YouTube do aplicativo.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API)
* que é retornado pelo servidor do YouTube com
* os dados do último vídeo liberado no canal
* vinculado ao app.
*
* Apesar de outros dados de snippet estarem
* disponíveis no JSON, o que importa para o
* aplicativo são: título, descrição e thumbs.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property title título do vídeo.
* @property description descrição do vídeo.
* @property thumbnails contém todas as versões
* disponíveis de thumb do vídeo.
* @constructor cria um objeto completo do tipo
* [SnippetParse].
*/
class SnippetParse(
val title: String,
val description: String,
val thumbnails: ThumbnailParse )

 

No mesmo pacote adicione agora a classe ItemParse com o seguinte código fonte:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém todos os dados necessários, em app, do
* último vídeo liberado no canal YouTube do
* aplicativo.
*
* O objetivo desta classe (objetos desta classe)
* é apenas de ser parte do parse do JSON (via
* Gson API) que é retornado pelo servidor do
* YouTube com os dados do último vídeo liberado
* do canal vinculado ao app.
*
* Apesar de outros dados estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente o id e alguns dados
* contidos em snippet.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property snippet contém informações de título,
* descrição e thumb do vídeo.
* @property id contém o identificador único do
* vídeo.
* @constructor cria um objeto completo do tipo
* [ItemParse].
*/
class ItemParse(
val snippet: SnippetParse,
val id: IdParse )

 

Por fim, a última classe parse para obter os dados de último vídeo liberado em canal.

No mesmo pacote, /model/parse/video, adicione a classe VideoParse com o seguinte código:

package thiengo.com.br.canalvinciusthiengo.model.parse.video

/**
* Contém todos os dados necessários em aplicativo
* do último vídeo liberado no canal YouTube do app.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API)
* que é retornado pelo servidor do YouTube com
* os dados do último vídeo liberado do canal
* vinculado ao app.
*
* Apesar de outros dados estarem disponíveis no
* JSON, os que importam para o aplicativo são:
* o id, o título, a descrição e a thumb.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property items lista que contém somente o
* último vídeo liberado no canal.
* @constructor cria um objeto completo do tipo
* [VideoParse].
*/
data class VideoParse(
private val items: List<ItemParse> ){

/**
* @property id contém o identificador único do
* vídeo. O método get() foi sobrescrito pois é
* possível, mesmo que pouco provável, que [items]
* seja null ou esteja vazio e assim não haja
* dados de último vídeo. Porém devido ao
* algoritmo definido em get() nunca é retornado
* null para a propriedade [id] de [VideoParse].
* @return o identificador único do vídeo.
*/
val id: String
get() : String{
return if( !items.isNullOrEmpty() ){
items[0].id.videoId
}
else{
""
}
}

/**
* @property title contém o título do vídeo. O
* método get() foi sobrescrito pois é possível
* que [items] seja null ou esteja vazio e assim
* não haja dados de último vídeo. Porém devido
* ao algoritmo definido em get() nunca é
* retornado null para a propriedade [title] de
* [VideoParse].
* @return o título do vídeo.
*/
val title: String
get() : String{
return if( !items.isNullOrEmpty() ){
items[0].snippet.title
}
else{
""
}
}

/**
* @property description contém a descrição do
* vídeo. O método get() foi sobrescrito pois
* é possível que [items] seja null ou esteja
* vazio e assim não haja dados de último
* vídeo. Porém devido ao algoritmo definido
* em get() nunca é retornado null para a
* propriedade [description] de [VideoParse].
* @return a descrição do vídeo.
*/
val description: String
get() : String{
return if( !items.isNullOrEmpty() ){
items[0].snippet.description
}
else{
""
}
}

/**
* @property thumbUrl contém a URL da thumb do
* vídeo. O método get() foi sobrescrito pois
* é possível que [items] seja null ou esteja
* vazio e assim não haja dados de último
* vídeo. Porém devido ao algoritmo definido
* em get() nunca é retornado null para a
* propriedade [thumbUrl] de [VideoParse].
* @return a URl da thumb de alta resolução
* do vídeo.
*/
val thumbUrl: String
get() : String{
return if( !items.isNullOrEmpty() ){
items[0].snippet.thumbnails.high.url
}
else{
""
}
}
}

 

Em código de lógica de negócio, quando for necessário obter os dados de vídeo recolhidos dos servidores do YouTube, vamos utilizar diretamente somente a classe VideoParse.

Pois com ela já será possível acessar facilmente os dados de vídeo que o projeto precisa.

PlayLists

O JSON retornado com as informações de PlayLists disponíveis em canal é o seguinte:

{
"kind": "youtube#playlistListResponse",
"etag": "-vNxfrLYegmIzO6pekdIlwieO2I",
"nextPageToken": "CDIQAA",
"pageInfo": {
"totalResults": 56,
"resultsPerPage": 50
},
"items": [
{
"kind": "youtube#playlist",
"etag": "pzYK_ERYOdz4P5jcNpOpMVtk-tg",
"id": "PLBA57K2L2RIJeKoaLgTtYKSFAhvH6FAcG",
"snippet": {
"publishedAt": "2020-07-12T15:09:27Z",
"channelId": "UCG3gFuIkRF3PpNkRk3Wp6dw",
"title": "[MINI-CURSO] Porque e Como Utilizar Vetores no Android",
"description": "Este é um mini...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/yEFNwGP4SVA/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/yEFNwGP4SVA/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/yEFNwGP4SVA/hqdefault.jpg",
"width": 480,
"height": 360
},
"standard": {
"url": "https://i.ytimg.com/vi/yEFNwGP4SVA/sddefault.jpg",
"width": 640,
"height": 480
},
"maxres": {
"url": "https://i.ytimg.com/vi/yEFNwGP4SVA/maxresdefault.jpg",
"width": 1280,
"height": 720
}
},
"channelTitle": "Vinícius Thiengo",
"localized": {
"title": "[MINI-CURSO] Porque e Como Utilizar Vetores no Android",
"description": "Este é um..."
}
}
},
...
]
}

 

Para obter os dados de PlayLists importantes ao nosso projeto vamos ter que criar mais três classes parse.

Com essas classes nosso objetivo será obter os seguintes dados de cada PlayList:

  • Identificador único ➙ playListsObj.items[ position ].id;
  • e Título ➙ playListsObj.items[ position ].snippet.title.

Sendo assim...

... no pacote /model/parse crie um novo pacote /playlist.

Neste novo pacote crie a classe SnippetParse com o seguinte código:

package thiengo.com.br.canalvinciusthiengo.model.parse.playlist

/**
* Contém o título da PlayList.
*
* O objetivo desta classe (objetos desta classe)
* é apenas de ser parte do parse do JSON (via
* Gson API) que é retornado pelo servidor do
* YouTube com os dados de PlayLists do canal
* vinculado ao app.
*
* Apesar de outros dados de snippet estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente o título (title).
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property title título da PlayList.
* @constructor cria um objeto completo do tipo
* [SnippetParse].
*/
class SnippetParse( val title: String )

 

Em seguida, no mesmo pacote, crie a classe ItemParse com o código fonte a seguir:

package thiengo.com.br.canalvinciusthiengo.model.parse.playlist

/**
* Contém todos os dados necessários, em app, de
* uma PlayList.
*
* O objetivo desta classe (objetos desta classe)
* é apenas de ser parte do parse do JSON (via
* Gson API) que é retornado pelo servidor do
* YouTube com os dados de PlayLists do canal
* vinculado ao app.
*
* Apesar de outros dados de item estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente o id e o título.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property id identificador único da PlayList.
* @property snippet contém a informação de
* título da PlayList.
* @constructor cria um objeto do tipo
* [ItemParse].
*/
class ItemParse(
val id: String,
private val snippet: SnippetParse? ){

/**
* @property title contém o título da PlayList.
* O método get() foi sobrescrito pois é possível,
* mesmo que pouco provável, que [snippet] seja
* null e assim não haja título de PlayList.
* Porém devido ao algoritmo definido em get()
* nunca é retornado null para a propriedade
* [title] de [ItemParse].
* @return uma [String] válida de título de
* [PlayList].
*/
val title: String
get() : String {
return if( snippet != null ){
snippet.title
}
else{
""
}
}
}

 

Por fim a classe parse que teremos acesso direto em lógica de negócio, a classe PlayListsParse.

Crie está com o código a seguir e também no pacote /model/parse/playlist:

package thiengo.com.br.canalvinciusthiengo.model.parse.playlist

/**
* Contém os dados necessários em aplicativo de
* todas as PlayLists retornadas do servidor de
* dados do YouTube.
*
* O objetivo desta classe (objetos desta classe)
* é ser parte do parse do JSON (via Gson API)
* que é retornado pelo servidor do YouTube com
* os dados de PlayLists do canal vinculado ao
* app.
*
* Apesar de outros dados de PlayLists estarem
* disponíveis no JSON, o que importa para o
* aplicativo é somente o id e o título de cada
* PlayList dentro da lista items.
*
* Com a configuração atual de uso da API Gson
* para aplicação do parse JSON, as propriedades de
* classes parse devem ter os mesmos rótulos dos
* campos referentes a elas em JSON.
*
* @property items PlayLists retornadas pelo
* YouTube Data API.
* @constructor cria um objeto completo do tipo
* [PlayListsParse].
*/
class PlayListsParse( val items: List<ItemParse> )

 

É isso.

As classes parse necessárias em projeto para auxílio às APIs de comunicação remota, todas essas estão prontas.

Interface de comunicação

Agora podemos criar a Interface que vai permitir a Retrofit API se comunicar com os servidores do YouTube.

Na raiz do projeto crie o pacote /network.

Em seguida crie a Interface YouTubeService com o código fonte a seguir:

package thiengo.com.br.canalvinciusthiengo.network

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
import thiengo.com.br.canalvinciusthiengo.config.YouTubeConfig
import thiengo.com.br.canalvinciusthiengo.model.parse.playlist.PlayListsParse
import thiengo.com.br.canalvinciusthiengo.model.parse.video.VideoParse

/**
* Interface responsável por definir a API de comunicação
* remota do aplicativo para ser utilizada junto à
* biblioteca Retrofit.
*/
interface YouTubeService {

/* TODO */
}

 

Pelos imports já é possível ao menos deduzir que mais códigos serão adicionados a essa "Interface Serviço".

Primeiro vamos adicionar a API que permite obter um objeto VideoParse da YouTube Data API. Adicionar em YouTubeService:

...
/**
* Invoca dados de "último vídeo" disponível de acordo
* com os parâmetros query informados.
*
* @param key chave de API de desenvolvedor Google.
* @param channelId identificador único do canal.
* @param part trecho de dados que deve estar presente
* em recurso vídeo retornado.
* @param maxResults número máximo de videos.
* @param order dado de ordenação dos resultados.
* @return todos os dados da resposta do servidor
* remoto - em objeto Call<VideoParse>.
*/
@GET( value = YouTubeConfig.ApiV3.VIDEO_PATH )
fun lastVideo(
@Query("key")
key: String = YouTubeConfig.Key.GOOGLE_DEV,
@Query("channelId")
channelId: String = YouTubeConfig.Channel.CHANNEL_ID,
@Query("part")
part: String = YouTubeConfig.ApiV3.PART_PARAM,
@Query("maxResults")
maxResults: String = YouTubeConfig.ApiV3.MAX_RESULTS_VIDEO_PARAM,
@Query("order")
order: String = YouTubeConfig.ApiV3.ORDER_PARAM
): Call<VideoParse>
...

 

Enfim as constantes de YouTubeConfig.ApiV3 sendo utilizadas "em massa".

Agora a API que permite a Retrofit solicitar ao YouTube uma PlayListsParse com todas as PlayLists do canal:

...
/**
* Invoca dados de PlayLists disponíveis de acordo
* com os parâmetros query informados.
*
* @param key chave de API de desenvolvedor Google.
* @param channelId identificador único do canal.
* @param part trecho de dados que deve estar presente
* em recurso vídeo retornado.
* @param maxResults número máximo de PlayLists.
* @param order dado de ordenação dos resultados.
* @return todos os dados da resposta do servidor
* remoto - em objeto Call<PlayListsParse>.
*/
@GET( value = YouTubeConfig.ApiV3.PLAYLISTS_PATH )
fun playLists(
@Query("key")
key: String = YouTubeConfig.Key.GOOGLE_DEV,
@Query("channelId")
channelId: String = YouTubeConfig.Channel.CHANNEL_ID,
@Query("part")
part: String = YouTubeConfig.ApiV3.PART_PARAM,
@Query("maxResults")
maxResults: String = YouTubeConfig.ApiV3.MAX_RESULTS_PLAYLISTS_PARAM,
@Query("order")
order: String = YouTubeConfig.ApiV3.ORDER_PARAM
): Call<PlayListsParse>
...

 

É isso, nossa Interface Serviço de comunicação com os servidores do YouTube está finalizada.

Agora podemos ir às entidades de configuração de:

  • Tipos de comunicação;
  • e Possíveis problemas em conexão.

Encapsulando estados importantes

Em nosso algoritmo de comunicação remota teremos conexão síncrona e assíncrona.

Pois (olha o spoiler) em alguns trechos do projeto a invocação à comunicação remota já estará ocorrendo em uma Thread secundária.

Dessa forma não tem porque colocar a conexão ainda em uma outra nova Thread.

Sendo assim, no pacote /network, crie a enum class NetworkRequestMode a seguir:

package thiengo.com.br.canalvinciusthiengo.network

/**
* Contém os possíveis modelos de requisição remota
* utilizados junto a biblioteca Retrofit.
*
* Ambos os modelos são utilizados em projeto.
*/
enum class NetworkRequestMode {
SYNCHRONOUS,
ASYNCHRONOUS
}

 

Certo.

Um outro ponto de todo o algoritmo de requisição remota é: a conexão pode falhar.

E algumas dessas falhas nós iremos ter um retorno específico em tela. Mas precisamos saber qual falha ocorreu.

Sendo assim, ainda em /network, crie a enum class NetworkRetrieveDataProblem como a seguir:

package thiengo.com.br.canalvinciusthiengo.network

/**
* Contém os possíveis problemas que podem ocorrer
* em comunicação remota e que serão trabalhados pelo
* aplicativo.
*/
enum class NetworkRetrieveDataProblem {
NO_VIDEO,
NO_PLAYLISTS,
NO_INTERNET_CONNECTION
}

 

Lembrando que para estados de objetos e para estados de processamento. Para estes nós vamos manter o uso de classes enum, pois a leitura do código fica facilitada.

Um outro ponto:

Apesar de termos definido NO_VIDEO e NO_INTERNET_CONNECTION. Para este primeiro release do app não haverá tratamento destes problemas em camada de UI.

Em resumo, o porquê do não tratamento desses possíveis problemas é devido à limitação de quotas da YouTube Data API.

De qualquer forma, vamos já deixa-los "sinalizados" em projeto para futuros releases.

Trabalhando a resposta backend

Agora podemos criar os Callbacks de vídeo e de PlayLists que trabalham a resposta retornada pelo YouTube.

Normalmente esses códigos são todos colocados em uma mesma classe, mas aqui nós ficaríamos com uma classe utilitária muito grande.

É isso, vamos aos códigos.

Resposta de último vídeo

Vamos primeiro à classe que trabalha a resposta backend de último vídeo disponível em canal.

No pacote /network crie a classe LastVideoResponse com o seguinte código fonte:

package thiengo.com.br.canalvinciusthiengo.network

import android.content.Context
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import thiengo.com.br.canalvinciusthiengo.data.dynamic.UtilDatabase
import thiengo.com.br.canalvinciusthiengo.model.LastVideo
import thiengo.com.br.canalvinciusthiengo.model.parse.video.VideoParse

/**
* Trabalha a resposta do servidor do YouTube à requisição
* de dados de um novo "último vídeo" liberado em canal.
*
* @property context contexto do aplicativo.
* @property callbackSuccess callback que deve ser
* executado em caso de resposta bem sucedida.
* @property callbackFailure callback que deve ser
* executado em caso de resposta falha.
* @constructor cria um objeto completo do tipo
* [LastVideoResponse].
*/
class LastVideoResponse(
private val context: Context,
private val callbackSuccess: (LastVideo)->Unit = {},
private val callbackFailure: (NetworkRetrieveDataProblem)->Unit = {}
) : Callback<VideoParse> {

override fun onResponse(
call: Call<VideoParse>,
response: Response<VideoParse> ){

/* TODO */
}

override fun onFailure(
call: Call<VideoParse>,
t: Throwable ){
callbackFailure( NetworkRetrieveDataProblem.NO_INTERNET_CONNECTION )
}
}

 

Os parâmetros callback (callbackSuccess e callbackFailure) são responsáveis por enviar a resposta backend já processada às entidades clientes (outros objetos em projeto) assim que ela está pronta.

Ou a resposta é de sucesso ou é de falha.

Ainda em LastVideoResponse adicione o método parse() como a seguir:

...
/**
* Cria um novo [LastVideo] em app (incluindo no banco de
* dados local) caso a resposta do YouTube à requisição
* de dados do "último vídeo" seja bem sucedida.
*
* @param response resposta do backend YouTube já com o
* parse Gson aplicado.
*/
fun parse( response: Response<VideoParse> ){

if( response.isSuccessful ){
val video = response.body()!!

if( video.id.isNotEmpty() ){
val lastVideo = LastVideo(
uid = video.id,
title = video.title,
description = video.description
).apply {
thumbUrl = video.thumbUrl
}

UtilDatabase
.getInstance( context = context )
.saveLastVideo( lastVideo = lastVideo )

callbackSuccess( lastVideo )
}
else{
callbackFailure( NetworkRetrieveDataProblem.NO_VIDEO )
}
}
else{
callbackFailure( NetworkRetrieveDataProblem.NO_INTERNET_CONNECTION )
}
}
...

 

Force NullPointerException (!!) está sendo utilizado em response.body()!!, pois nós conhecemos o fluxo do código e sabemos que naquele ponto do algoritmo o valor de body() não será null.

Note que mesmo já em parse() é possível ter erros, dessa forma ainda são necessárias algumas configurações com callbackFailure neste método.

Veja também que logo depois que o parse é realizado com sucesso os dados são imediatamente salvos em base local:

...
UtilDatabase
.getInstance( context = context )
.saveLastVideo( lastVideo = lastVideo )
...

 

Agora o que falta é adicionar a invocação a parse() em onResponse():

...
override fun onResponse(
call: Call<VideoParse>,
response: Response<VideoParse> ){

parse( response = response )
}
...

 

Com a configuração LastVideoResponse completa, podemos ir seguros para a classe PlayListsResponse.

Resposta de PlayLists

Exatamente como na classe LastVideoResponse, aqui a nossa proposta é isolar todo o algoritmo que trabalha o parse na resposta backend do YouTube à nossa solicitação por PlayLists disponíveis no canal.

Sendo assim, primeiro vamos criar a classe PlayListsResponse com os métodos obrigatórios de Callback.

Logo, em /network, crie a classe a seguir:

package thiengo.com.br.canalvinciusthiengo.network

import android.content.Context
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import thiengo.com.br.canalvinciusthiengo.data.dynamic.UtilDatabase
import thiengo.com.br.canalvinciusthiengo.model.PlayList
import thiengo.com.br.canalvinciusthiengo.model.parse.playlist.PlayListsParse

/**
* Trabalha a resposta do servidor do YouTube à requisição
* de dados de PlayLists liberadas no canal.
*
* @property context contexto do aplicativo.
* @property callbackSuccess callback que deve ser
* executado em caso de resposta bem sucedida.
* @property callbackFailure callback que deve ser
* executado em caso de resposta falha.
* @constructor cria um objeto completo do tipo
* [PlayListsResponse].
*/
class PlayListsResponse(
private val context: Context,
private val callbackSuccess: (List<PlayList>)->Unit = {},
private val callbackFailure: (NetworkRetrieveDataProblem)->Unit = {}
) : Callback<PlayListsParse> {

override fun onResponse(
call: Call<PlayListsParse>,
response: Response<PlayListsParse> ){

/* TODO */
}

override fun onFailure(
call: Call<PlayListsParse>,
t: Throwable ){

callbackFailure( NetworkRetrieveDataProblem.NO_INTERNET_CONNECTION )
}
}

 

Cada callback em algoritmo é:

Uma proposta fácil e pequena em código para enviar o resultado (já processado) para qualquer objeto do projeto que precise dele.

Ainda temos que adicionar o método parse() da classe:

...
/**
* Cria uma nova lista de PlayLists em app (incluindo no
* banco de dados local) caso a resposta do YouTube à
* requisição de dados de PlayLists disponíveis seja bem
* sucedida.
*
* @param response resposta do backend YouTube já com o
* parse Gson aplicado.
*/
fun parse( response: Response<PlayListsParse> ){

if( response.isSuccessful ){
val playListParse = response.body()!!

if( playListParse.items.isNotEmpty() ){

val playLists = mutableListOf<PlayList>()

playListParse.items.map{
playLists.add(
PlayList(
uid = it.id,
title = it.title
)
)
}

UtilDatabase
.getInstance( context = context )
.savePlayLists( playLists = playLists )

callbackSuccess( playLists )
}
else{
callbackFailure( NetworkRetrieveDataProblem.NO_PLAYLISTS )
}
}
else{
callbackFailure( NetworkRetrieveDataProblem.NO_INTERNET_CONNECTION )
}
}
...

 

Como em LastVideoResponse, o código faz o "feijão com arroz" 🍛:

Verifica se a resposta do backend está ok.

Estando ok, aplica o parse, salva os dados em banco de dados e invoca o callback de sucesso.

Ainda falta invocar parse() em onResponse():

...
override fun onResponse(
call: Call<PlayListsParse>,
response: Response<PlayListsParse> ){

parse( response = response )
}
...

 

Com isso podemos enfim seguir à classe utilitária que vai permitir o fácil acesso aos algoritmos de comunicação remota em qualquer parte do projeto.

Classe utilitária

Novamente a nossa estratégia de classe utilitária, que permite o fácil acesso aos algoritmos essenciais em qualquer parte do projeto.

Logo, no pacote /network, crie a classe UtilNetwork com o código inicial a seguir:

package thiengo.com.br.canalvinciusthiengo.network

import android.content.Context
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import thiengo.com.br.canalvinciusthiengo.config.YouTubeConfig
import thiengo.com.br.canalvinciusthiengo.model.LastVideo
import thiengo.com.br.canalvinciusthiengo.model.PlayList

/**
* Classe utilitária que permite fácil acesso à
* API de comunicação remota do aplicativo.
*
* Assim é possível obter de maneira imediata e
* não verbosa uma nova comunicação remota.
*
* @property context contexto do aplicativo.
* @constructor cria um objeto completo do tipo
* [UtilNetwork].
*/
class UtilNetwork private constructor(
private val context: Context ){

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

/**
* Método que aplica, junto à propriedade
* [instance], o padrão Singleton em classe.
* Garantindo que somente uma instância de
* [UtilNetwork] 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 [UtilNetwork].
*/
fun getInstance( context: Context ) : UtilNetwork {
if( instance == null ){
instance = UtilNetwork( context = context )
}
return instance!!
}
}
}

 

Aquela mesma ideia em projeto para nossas classes utilitárias: utilizando o padrão Singleton.

Agora vamos adicionar o código que permite a fácil geração de um objeto Retrofit de serviço.

Ainda na classe UtilNetwork adicione o método getYouTubeService(), fora do companion object, como a seguir:

...
/**
* Retorna um objeto de serviço completo para comunicação
* remota com o servidor de dados do YouTube.
*
* @return serviço Retrofit para comunicação remota.
*/
private fun getYouTubeService()
= Retrofit.Builder()
.baseUrl( YouTubeConfig.ApiV3.BASE_URL )
.addConverterFactory( GsonConverterFactory.create() )
.build()
.create( YouTubeService::class.java )
...

 

Agora os códigos de obtenção, via callback, de dados de vídeo e de PlayLists.

Recuperando objeto LastVideo

Os métodos que vamos criar a partir daqui e que iniciam os rótulos com retrieve e são de acesso public.

Esses métodos são as entidades de nossa classe utilitária que serão invocados em inúmeros trechos do projeto.

Vamos iniciar com o método retrieveLastVideo() ainda na classe UtilNetwork e fora de companion object:

...
/**
* Realiza a comunicação remota com o servidor de dados do
* YouTube para obter os dados do "último vídeo" liberado
* em canal.
*
* @param networkRequestMode modelo de conexão com o servidor
* remoto.
* @param callbackSuccess callback que deve ser executado
* em caso de resposta bem sucedida.
* @param callbackFailure callback que deve ser executado
* em caso de resposta falha.
*/
fun retrieveLastVideo(
networkRequestMode: NetworkRequestMode = NetworkRequestMode.ASYNCHRONOUS,
callbackSuccess: (LastVideo)->Unit = {},
callbackFailure: (NetworkRetrieveDataProblem)->Unit = {} ){

val service = getYouTubeService()
val call = service.lastVideo()
val lastVideoResponse = LastVideoResponse(
context = context,
callbackSuccess = callbackSuccess,
callbackFailure = callbackFailure
)

if( networkRequestMode == NetworkRequestMode.ASYNCHRONOUS ){
call.enqueue( lastVideoResponse )
}
else{
try{
lastVideoResponse.parse(
response = call.execute()
)
}
catch( e: Exception ){}
}
}
...

 

A invocação a call.execute() pode gerar uma exceção. Por isso o try...catch:

...
try{
lastVideoResponse.parse(
response = call.execute()
)
}
catch( e: Exception ){}
...

 

Não trabalharemos está exceção nem no código de recuperação de LastVideo e nem no código de recuperação de lista de PlayLists.

Isso, pois a própria API Retrofit já tem internamente algoritmos de re-tentativas de chamadas em caso de falhas e a informação principal do app em nosso domínio, último vídeo liberado.

Está informação é principalmente entregue via criação de notificação push em Dashboard OneSignal.

Assim podemos partir para o código de recuperação de PlayLists.

Recuperando PlayLists

O código aqui é muito similar ao de LastVideo.

Ainda em UtilNetwork, fora de companion object, crie o método retrievePlayLists() com o código a seguir:

...
/**
* Realiza a comunicação remota com o servidor de dados do
* YouTube para obter os dados de PlayLists disponíveis
* em canal.
*
* @param networkRequestMode modelo de conexão com o servidor
* remoto.
* @param callbackSuccess callback que deve ser executado
* em caso de resposta bem sucedida.
* @param callbackFailure callback que deve ser executado
* em caso de resposta falha.
*/
fun retrievePlayLists(
networkRequestMode: NetworkRequestMode = NetworkRequestMode.ASYNCHRONOUS,
callbackSuccess: (List<PlayList>)->Unit = {},
callbackFailure: (NetworkRetrieveDataProblem)->Unit = {} ){

val service = getYouTubeService()
val call = service.playLists()
val playListsResponse = PlayListsResponse(
context = context,
callbackSuccess = callbackSuccess,
callbackFailure = callbackFailure
)

if( networkRequestMode == NetworkRequestMode.ASYNCHRONOUS ){
call.enqueue( playListsResponse )
}
else{
try{
playListsResponse.parse(
response = call.execute()
)
}
catch( e: Exception ){}
}
}
...

 

Agora o que temos que fazer é configurar as invocações de comunicação remota em pontos estratégicos do projeto.

Até este ponto de nosso app, framework, Android temos a seguinte configuração física (pacotes das entidades de código dinâmico que foram adicionadas) em IDE:

Configuração física do projeto Android

Próximo conteúdo

Mais um importante conteúdo de configuração adicionado a projeto.

Nosso próximo passo é permitir que o algoritmo de comunicação remota possa ser invocado em pontos estratégicos e importantes de todo o app.

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

➙ Configurando o WorkManager Para Requisições em Background - Parte 12.

Então é isso.

Depois de mais este longo conteúdo, descanse um pouco 💆‍♂. Tome um café ☕ 🥓 🥭. E...

... te vejo na Parte 12 do projeto.

Se houverem dúvidas ou dicas deste décimo primeiro 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

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Fontes em XML, Android O. Configuração e UsoFontes em XML, Android O. Configuração e UsoAndroid
Lottie API Para Animações no AndroidLottie API Para Animações no AndroidAndroid
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...