
Desenvolvendo a Tela e a Lógica de PlayLists - YouTuber Android App - Parte 8
(1697)

CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim

CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
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