Desenvolvendo a Tela e a Lógica de PlayLists - YouTuber Android App - Parte 8
(2429)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Tudo bem?
Neste artigo vamos continuar com o nosso projeto de aplicativo Android para YouTubers.
Nesta parte oito do projeto nós vamos construir passo a passo toda a configuração da tela de PlayLists do 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 oitavo 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:
- Construa Um Aplicativo Android Completo Para YouTubers - Parte 1;
- Início do Lado Tático e Barra de Topo Personalizada - Parte 2;
- Criando e Configurando o Menu Principal - Parte 3;
- Criando a Estrutura Base Das Telas Com Lista - Parte 4;
- Construindo os Fragmentos de Conteúdo Local - Parte 5;
- Banco de Dados Local Com a Room API - Parte 6;
- Construindo a Tela e a Lógica de Último Vídeo - Parte 7;
- Desenvolvendo a Tela e a Lógica de PlayLists - Parte 8 (você está aqui);
- Vinculando Telas ao Menu Principal - Parte 9;
- Configurando Por Completo o Sistema de Notificação Push - Parte 10;
- Configurando a YouTube Data API Com a Biblioteca Retrofit - Parte 11;
- Configurando o WorkManager Para Requisições em Background - Parte 12;
- Testes e Resultados no Projeto Finalizado - Parte 13;
- Nós Temos Um Framework Em Mãos - Parte 14;
- Como e Onde Monetizar o Aplicativo Framework - Parte 15.
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.
Fragmento de PlayLists
A tela de PlayLists do canal é uma das telas opcionais em projeto.
Foi escolhido colocar está tela para essa versão de app, pois as PlayLists do canal Vinícius Thiengo são muito acessadas pelos seguidores.
Aqui vamos a construção passo a passo da primeira parte do fragmento de PlayLists:
Digo primeira parte, pois como vai ocorrer também com o fragmento de último vídeo, assim que colocarmos em projeto os algoritmos de comunicação com a YouTube Data API nós teremos que dar um improve neste fragmento.
Então é isso. Vamos aos códigos.
Estáticos de interface
Como de costume, vamos iniciar com os códigos estáticos XML. Pois as atualizações deles são mais simples.
Rótulos
No arquivo de Strings, /res/values/strings.xml, adicione as seguintes novas definições:
...
<!-- PlayListsFragment -->
<string name="playlists_content_title">
PlayLists do canal
</string>
<string name="playlist_toast_alert">
É preciso ou o aplicativo do YouTube ou um navegador
Web para que a PlayList \"%s\" seja acessada.
</string>
<string name="no_playlists_yet">
Nenhuma PlayList disponível no canal!
</string>
...
Note que agora também trabalharemos com a possibilidade de conteúdo vazio:
...
<string name="no_playlists_yet">
Nenhuma PlayList disponível no canal!
</string>
...
Pois diferente do fragmento de último vídeo liberado...
... aqui é possível que o canal não tenha de início nenhuma PlayList ativa, mas posteriormente venha a adicionar PlayLists.
Ícones vetoriais
São apenas dois ícones exclusivos deste fragmento de PlayLists.
Um de item de lista e outro para quando não tem PlayList em canal.
Ícone de item de lista PlayList (ícone para todas as PlayLists em tela), ic_playlist_color.xml:
Ícone de "não há PlayLists em canal", ic_no_content.xml:
Como padrão na construção desse aplicativo: realize o download dos ícones anteriores e coloque-os no folder /res/drawable de sua versão de projeto.
Layout para tela vazia
Quando não houver ao menos uma PlayList no canal, então uma tela de "no content" será apresentada:
Sendo assim teremos um "layout" extra que será carregado dentro do layout de fragmentos de PlayLists.
Esse layout de no content na verdade também terá um ProgressBar para quando o conteúdo estiver sendo carregado em uma Thread fora da Thread Principal.
Vamos à estrutura deste layout de no content:
Agora, no folder /res/layout, crie o arquivo XML no_data_message_block.xml com o seguinte código fonte:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl_no_data_message_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="@dimen/standard_screen_side_margin"
android:layout_marginEnd="@dimen/standard_screen_side_margin"
android:layout_marginBottom="25dp"
android:layout_weight="1">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/pb_load_content"
style="?android:attr/progressBarStyleLarge"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_centerInParent="true"
android:visibility="visible" />
<TextView
android:id="@+id/tv_no_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:drawableStart="@drawable/ic_no_content"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:textColor="@color/colorContentText"
android:visibility="gone" />
</RelativeLayout>
Com a definição de android:layout_weight no ViewGroup container do layout anterior já fica explícito que ele será carregado dentro de um LinearLayout.
Com o estilo nativo progressBarStyleLarge no ProgressBar temos o seguinte resultado em tela (quando o conteúdo esta carregando e esta lento):
Layout
Por fim o layout principal do fragmento de PlayLists.
Primeiro o diagrama da estrutura dele:
Agora, no folder /res/layout, crie o arquivo fragment_play_lists.xml com os fontes a seguir:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.fragment.LastVideoFragment">
<TextView
style="@style/AppTheme.Title"
android:text="@string/playlists_content_title" />
<include layout="@layout/no_data_message_block" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_play_lists"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone" />
</LinearLayout>
Se você estava esperando também um o layout de item de lista...
... então lembre que a classe de domínio PlayList implementa a Interface ListItem, pois iremos utilizar o adapter ListItemAdapter também no fragmento de PlayLists.
Desta forma não precisamos mais nos preocupar com classe adaptadora, ViewHolder e layout de item.
LastVideoFragment
Com isso podemos ir ao código dinâmico da tela de PlayLists.
Vamos pouco a pouco construindo esse fragmento.
Agora, no pacote /ui/fragment, crie o fragmento PlayListsFragment com o seguinte código inicial:
package thiengo.com.br.canalvinciusthiengo.ui.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_play_lists.*
import kotlinx.android.synthetic.main.no_data_message_block.*
import thiengo.com.br.canalvinciusthiengo.R
import thiengo.com.br.canalvinciusthiengo.data.dynamic.UtilDatabase
import thiengo.com.br.canalvinciusthiengo.data.fixed.PlayListsData
import thiengo.com.br.canalvinciusthiengo.model.PlayList
import thiengo.com.br.canalvinciusthiengo.ui.adapter.ListItemAdapter
/**
* Contém toda a UI de PlayLists do canal YouTube
* do app.
*
* @constructor cria um objeto completo do tipo
* [PlayListsFragment].
*/
class PlayListsFragment : Fragment() {
companion object {
/**
* Constante com o identificador único do
* fragmento [PlayListsFragment] para que
* ele seja encontrado na pilha de fragmentos
* e assim não seja necessária a construção
* de mais de um objeto deste fragmento em
* memória enquanto o aplicativo estiver em
* execução.
*/
const val KEY = "PlayListsFragment_key"
}
/**
* [playLists] sempre inicia com alguma lista
* mutável válida de PlayLists, mesmo que nenhuma
* PlayList tenha sido ainda enviada ao app e a
* lista esteja vazia.
*/
private val playLists = mutableListOf<PlayList>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? ) : View? {
return inflater.inflate(
R.layout.fragment_play_lists,
container,
false
)
}
}
Note que neste fragmento não há herança de FrameworkListFragment.
Pois aqui também teremos inúmeros outros códigos que trabalham com dados carregados de maneira assíncrona e assim exigem uma configuração de acesso a UI (ao menos neste primeiro release) distante do que foi definido em FrameworkListFragment.
De qualquer forma, os componentes de framework de lista serão todos reaproveitados.
Callback de acesso à PlayList
Antes de prosseguirmos para o código de inicialização de lista, vamos primeiro criar a função responsável por abrir a PlayList no aplicativo nativo do YouTube ou no site via navegador mobile.
Em PlayListsFragment coloque o seguinte novo método:
...
/**
* Invoca o aplicativo do YouTube para que o usuário
* tenha acesso à PlayList do canal que foi acionada.
*
* Caso o dado de URI presente no objeto [playList] seja
* inválido para a abertura do app nativo do YouTube
* ou abertura da versão dele em app de navegador Web,
* então uma mensagem de falha é apresentada.
*
* @param playList objeto [PlayList] do item de lista
* acionado pelo usuário.
*/
private fun callYouTubePlayListCallback(
playList: PlayList ){
val intent = Intent(
Intent.ACTION_VIEW,
playList.getAppUri()
)
/**
* É utópico, mas pode ocorrer de não haver
* instalado no aparelho do usuário o aplicativo
* do YouTube e nem mesmo um navegador Web.
*
* Sendo assim, ao invés de gerar uma exceção,
* nós avisamos ao usuário a necessidade de
* instalar o aplicativo adequado.
*/
if( intent.resolveActivity( activity!!.packageManager ) == null ){
Toast
.makeText(
activity,
String.format(
getString( R.string.playlist_toast_alert ),
playList.title
),
Toast.LENGTH_LONG
)
.show()
return
}
activity!!.startActivity( intent )
}
...
Iniciando a lista em tela
Em PlayListsFragment coloque o seguinte método para a inicialização do RecyclerView:
...
/**
* Inicializa por completo o framework de lista
* [RecyclerView] que deve conter a listagem das
* PlayLists do canal.
*/
private fun initPlayListList(){
val layoutManager = LinearLayoutManager( activity )
rv_play_lists?.layoutManager = layoutManager
rv_play_lists?.setHasFixedSize( true )
rv_play_lists?.adapter = ListItemAdapter(
context = activity!!,
items = playLists,
callExternalAppCallback = {
item -> callYouTubePlayListCallback(
playList = item as PlayList
)
}
)
}
...
Note que novamente estaremos utilizando o operador force null (!!). Pois onde ele aparecer (por exemplo: activity!!) é porque conhecemos o fluxo do código e sabemos que nunca será realmente null.
Esse force NullPointerException aparece com frequência em todo o projeto, pois estamos quase sempre utilizando propriedades nativas criadas em códigos Java (que não são null safe) e então referenciando elas em código Kotlin not null.
O Lint do Android Studio fica "reclamando" se o nosso código não tiver um "roteiro de resposta" para um possível null onde não deve ter null nunca.
E o nosso roteiro de resposta "mais clean" é: se for null, então gere um NullPointerException.
Mas é aquilo, estamos utilizando !! onde sabemos que não será null 😁.
Outro ponto relevante do código anterior é o casting sendo aplicado em item:
...
callExternalAppCallback = {
item -> callYouTubePlayListCallback(
playList = item as PlayList
)
}
...
O callback callYouTubePlayListCallback espera um objeto do tipo PlayList e sabemos que item é do tipo PlayList. Então: safe casting.
Agora vamos colocar o método initPlayListList() no local correto do fragmento, no método do ciclo de vida onActivityCreated():
...
override fun onActivityCreated( savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
initPlayListList()
}
...
Ainda faltam os métodos de acesso direto aos componentes de UI. Métodos que também serão invocados junto às APIs de banco de dados local.
Dados: carregando ou não?
E ai? Como a tela fica quando os dados estão na verdade sendo carregados fora da Thread Principal?
Ainda não temos um método que responde à está pergunta.
Não tínhamos.
No fragmento PlayListsFragment coloque o método uiDataStatus() com o código fonte a seguir:
...
/**
* Configura toda a UI, layout, do fragmento de
* acordo com o estado atual dos dados que devem
* ser apresentados em tela.
*
* Como é possível ter a invocação deste método
* fora da Thread Principal, então é importante
* sempre ter o código de atualização de UI
* dentro de runOnUiThread().
*
* Outro ponto importante é garantir que não
* haverá NullPointerException caso os dados
* cheguem em método quando a UI não mais está no
* foreground (primeiro plano). Assim o operador
* not null (?.) é utilizado com frequência.
*
* @param status estado atual dos dados que
* devem estar em tela.
*/
private fun uiDataStatus( status: UiFragLoadDataStatus ){
activity?.runOnUiThread {
var rvPlayLists = View.GONE
var rlNoDataMessageContainer = View.VISIBLE
var tvNoDataStatus = View.GONE
pb_load_content?.hide()
when( status ){
UiFragLoadDataStatus.LOADING -> {
pb_load_content?.show()
}
UiFragLoadDataStatus.NO_MAIN_CONTENT -> {
tv_no_data?.text = getString( R.string.no_playlists_yet )
tvNoDataStatus = View.VISIBLE
}
else -> {
rlNoDataMessageContainer = View.GONE
rvPlayLists = View.VISIBLE
}
}
rv_play_lists?.visibility = rvPlayLists
rl_no_data_message_container?.visibility = rlNoDataMessageContainer
tv_no_data?.visibility = tvNoDataStatus
}
}
...
Neste novo método o layout "no content" já começa a ser trabalhado.
Novamente, como no fragmento LastVideoFragment, estamos colocando todo o acesso de atualização da UI dentro de runOnUiThread().
Isso, pois esse é um daqueles métodos que poderão ser invocados a partir de uma Thread secundária.
E novamente o trabalho "intenso" com o operador not null, (?.).
Isso, para garantir que não teremos NullPointerException devido a acesso atrasado a objeto de componente visual que nem mais está em tela. Pois o usuário mudou de opção selecionando outra no menu principal do aplicativo.
E antes que agente se esqueça...
... ainda falta definir o enum de possíveis estados dos dados em tela.
No pacote /ui/fragment crie a enum class UiFragLoadDataStatus com o seguinte código:
package thiengo.com.br.canalvinciusthiengo.ui.fragment
/**
* Contém os possíveis estados da UI dos fragmentos
* que têm dados carregados de maneira assíncrona.
*/
enum class UiFragLoadDataStatus {
LOADING,
LOADED,
NO_MAIN_CONTENT
}
Como em outros pontos do projeto, optei por utilizar um enum. Pois no meu ponto de vista a leitura do código fica facilitada, dispensando comentários extras nos fontes.
Ainda temos um outro método UI para configurar em fragmento.
Dados completos em playLists
Ainda há a necessidade de colocarmos dados na propriedade playLists.
Principalmente devido à possibilidade de os dados de PlayLists serem entregues após o acesso à base de dados local em Thread secundária.
Para isso teremos no fragmento PlayListsFragment um método setUiModel() como a seguir:
...
/**
* Responsável principalmente pela configuração
* da lista de PlayLists em tela.
*
* Como é possível que a invocação deste método
* ocorra fora da Thread Principal, então é
* importante sempre ter o código de atualização
* de lista dentro de runOnUiThread().
*
* Outro ponto importante é garantir que não
* haverá NullPointerException caso os dados
* cheguem em método quando a UI não mais está no
* foreground (primeiro plano). Assim o operador
* not null (?.) é utilizado com frequência.
*
* @param pLists lista não mutável de PlayLists.
*/
private fun setUiModel( pLists: List<PlayList>? ){
if( !pLists.isNullOrEmpty() ){
activity?.runOnUiThread {
uiDataStatus(
status = UiFragLoadDataStatus.LOADED
)
if (!pLists.equals( playLists )) {
playLists.clear()
playLists.addAll( pLists )
}
rv_play_lists
?.adapter
?.notifyDataSetChanged()
}
}
else{
uiDataStatus(
status = UiFragLoadDataStatus.NO_MAIN_CONTENT
)
}
}
...
Note que no código anterior, para atualizarmos a UI de lista de itens, precisamos somente do fonte:
...
rv_play_lists
?.adapter
?.notifyDataSetChanged()
...
Isso, pois a propriedade playLists é uma lista mutável.
Ou seja, os objetos PlayList podem mudar dentro da lista, mas o objeto de lista continuará sendo o mesmo em memória.
Caso estivéssemos trabalhando com uma lista não mutável, listOf(), o código de atualização de lista de itens em tela seria muito mais complexo do que uma simples invocação a notifyDataSetChanged().
E, novamente, como estamos atualizando componentes de UI... o uso de runOnUiThread() e not null (?.) se faz necessário.
Agora, no onActivityCreated(), vamos invocar o setUiModel():
...
override fun onActivityCreated( savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
initPlayListList()
if( playLists.isNotEmpty() ){
setUiModel( pLists = playLists )
}
}
...
Por fim, podemos ir à lógica de negócio para acesso aos dados em banco de dados local, Room API.
Dados da base de dados local
Para acesso às PlayLists em base local a lógica será um pouco diferente dá lógica utilizada em LastVideoFragment.
Primeiro porque aqui podemos sim ter uma lista vazia.
Segundo porque temos que também atualizar em tela o status dela (tem ou não dados para serem apresentados?).
Mas acredite, será bem simples.
Ainda no método onActivityCreated() adicione o bloco else do if já definido.
Adicione esse bloco como a seguir:
...
override fun onActivityCreated( savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
initPlayListList()
if( playLists.isNotEmpty() ){
setUiModel( pLists = playLists )
}
else{
/**
* Todo o algoritmo abaixo é necessário aqui,
* pois na primeira abertura do aplicativo,
* quando acessando o fragmento [PlayListsFragment],
* é possível que a inserção de dados de PlayList
* no banco de dados local (partindo dos algoritmos
* em [InitialDataCallback]) não seja rápida o
* suficiente para os dados já serem apresentados
* neste fragmento quando o usuário estiver
* acessando-o pela primeira vez.
* */
playLists.addAll( PlayListsData.getInitialPlayLists() )
uiDataStatus( status = UiFragLoadDataStatus.LOADING )
UtilDatabase
.getInstance( context = activity!!.applicationContext )
.getAllPlayLists{
val auxPlayList = if( it.isNullOrEmpty() ) {
playLists
}
else {
it
}
setUiModel( pLists = auxPlayList )
}
}
}
...
O que vai ficar faltando de algoritmo direto neste fragmento de PlayLists é o código de requisição remota de dados, via YouTube Data API.
Mas ainda chegaremos a este ponto.
Antes disso, com todos os fragmentos já definidos em projeto, podemos configurar na MainActivity todo o código de acesso a fragmentos via opções de menu principal.
Então vamos a está configuração.
Até este ponto do projeto temos a seguinte configuração física (pacotes das entidades de código dinâmico que foram adicionadas) em IDE:
Próximo conteúdo
Excelente! Finalizamos a configuração inicial do fragmento da tela de PlayLists.
No próximo conteúdo vamos ao vinculo dos fragmentos já desenvolvidos às suas opções no menu principal do aplicativo.
Segue o link para acesso ao próximo conteúdo:
➙ Vinculando Telas ao Menu Principal - YouTuber Android App - Parte 9.
Então é isso.
Relaxe um pouco. Assista a alguns vídeos 📽. Tome um café ☕ 🧁 🍉. E...
... te vejo na Parte 9 do projeto.
Se houverem dúvidas ou dicas deste oitavo 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.
Comentários Facebook