Como Construir Aplicativos Android Com HTML e JSOUP
(10697) (4)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vlad Khononov
EditoraAlta Books
Edição1ª
Ano2024
Páginas320
Tudo bem?
Neste artigo vamos passo a passo implementar a library JSOUP em um aplicativo Android, aplicativo real. Assim será possível realizar o parser HTML e então obtermos os resultados de jogos de futebol que estão ocorrendo ao redor Mundo.
Com esses dados dinâmicos vindos de um site que somente trabalha com um domínio do problema, vamos ter, depois da implementação do JSOUP, um aplicativo nativo completo.
Com essa library Java você verá como é fácil caminhar por uma estrutura HTML utilizando, ou seletores CSS / jQuery, ou métodos DOM JavaScript.
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber, em primeira mão, todos os conteúdos Android exclusivos aqui.
A abaixo os tópicos que estaremos abordando:
- JSOUP, visão geral:
- Projeto de exemplo, Android:
- Atualização do aplicativo para requisições e processamentos JSOUP:
- Vídeo com implementação passo a passo do projeto;
- Conclusão;
- Fontes.
Caso você esteja interessado somente na parte de implementação do JSOUP, então não é necessário o estudo da seção e subseções de Projeto de exemplo, Android.
JSOUP, visão geral
JSOUP é uma library Java para realizar o parse HTML que, diferente das libraries de parse XML, é bem simples e permite a navegação igualmente é possível em códigos JavaScript / jQuery, por meio de seletores CSS.
Note que apesar do parse HTML não ser algo comum e até mesmo viável em aplicativos Android, algumas vezes essa é a única solução para obtermos os dados de um site e em seguida coloca-los em um aplicativo nativo, isso sem necessidade de trabalho com WebView.
Não entendi muito bem a parte "... apesar do parse HTML não ser algo comum e até mesmo viável", poderia explicar melhor?
O que eu quis dizer é que muitos sistemas Web já trabalham também com APIs JSON. Sendo assim essa é a melhor maneira de obter os dados desses sistemas.
O principal problema com o parse HTML é que subitamente a estrutura do site pode mudar e mesmo que seja uma pequena mudança, se essa for parte da seleção de elementos quando trabalhando com o parse HTML... Done! Seu algoritmo continua a funcionar, porém de maneira inconsistente.
Antes de prosseguir com os códigos de visão geral da library JSOUP, é importante ressaltar que a documentação, principalmente o cookbook JSOUP, são muito bem construídos e fáceis de se consumir, com menos de duas horas você consegue ler todo o conteúdo.
Recomendo que, para se aprofundar você vá direto ao cookbook: https://jsoup.org/cookbook/. De qualquer forma, trabalhando aqui com o JSOUP, em um projeto real, você já saberá o suficiente para utilizar essa library em seus próprios aplicativos Android.
Conexão
O código de conexão na verdade é o código de obtenção de conteúdo HTML.
E para a alegria dos programadores, o parser HTML já é realizado logo na conexão a fonte do conteúdo. Qualquer problema na estrutura HTML (nós sabemos que existem alguns, forçando o quirks mode em navegadores) é trabalhado para que não seja um problema na navegação pela estrutura de objetos no código Java, ou seja, o JSOUP junto a especificação WHATWG HTML5 tenta corrigir tudo que possível.
Segue código de obtenção de conteúdo:
/* QUANDO A FONTE É UMA VARIÁVEL DO TIPO STRING. */
String html = "<div><p>Lorem ipsum.</p></div>";
/* COLOCANDO O CÓDIGO HTML, NÃO COMPLETO, DENTRO DE UMA TAG <body>. */
Document doc1 = Jsoup.parseBodyFragment(html);
/* OBTENDO A TAG <body> E TODO O SEU CONTEÚDO. */
Element body = doc1.body();
/*
* AINDA QUANDO A FONTE É UMA VARIÁVEL DO TIPO STRING.
* PORÉM O CÓDIGO JÁ ESTÁ BEM ESTRUTURADO.
* */
html = "<html><head><title>TÍTULO</title></head><body><p>CONTEÚDO</p></body></html>";
Document doc2 = Jsoup.parse(html);
/*
* OBTENDO O CONTEÚDO HTML DE UMA FONTE EXTERNA.
* O PROTOCOLO PODE SER HTTP OU HTTPS. NOTE QUE
* post() TAMBÉM É UMA OPÇÃO ANTE A INVOCAÇÃO VIA get().
* */
Document doc3 = Jsoup.connect("http://www.superplacar.com.br/").get();
/* OBTENDO A TAG <title> E TODO O SEU CONTEÚDO */
Element title = doc2.getElementsByTag("title").get(0);
/*
* OBTENDO O CONTEÚDO HTML DE UM ARQUIVO LOCAL.
* O TERCEIRO PARÂMETRO DE parse() É OPCIONAL,
* COM ELE CONSEGUIMOS MUDAR URLS RELATIVAS
* PARA URLS ABSOLUTAS.
* */
File file = Environment.getExternalStorageDirectory();
file = new File( file.getAbsolutePath() + File.separator + "index.html");
Document doc4 = Jsoup.parse(file, "UTF-8", "http://www.superplacar.com.br/");
Note que sempre teremos objetos de algum dos seguintes tipos:
- Document: quando o parse foi realizado, mas ainda não houve seleção;
- Element: quando houve uma seleção que retorna somente um elemento. Utilizando o seletor ":eq", por exemplo;
- Elements: quando a seleção trouxer mais de um elemento. Utilizando o método getElementsByTag(), por exemplo;
- TextNode: quando quisermos obter os nodos de texto de um elemento. Utilizando o método textNodes(), por exemplo.
Note que para todas as classes anteriores é possível também criarmos os objetos e não somente obtermos eles, até porque o JSOUP permite a criação de estruturas HTML via código Java.
Todas as classes informadas, de forma direta ou não, herdam da classe Node.
Abaixo o código de exemplo no qual apresenta o que o JSOUP faz quando a estrutura HTML está mal formada:
/* UMA ESTRUTURA HTML COM PROBLEMAS, TAGS <p> SEM A CORRESPONDÊNCIA DE FECHAMENTO. */
String html = "<html><head><title>TÍTULO</title></head><body><p>LOREM <p>IPSUM</body></html>";
Document doc = Jsoup.parse(html);
/*
* COMO RESULTADO PARA NAVEGAÇÃO TEREMOS:
* <html><head><title>TÍTULO</title></head><body><p>LOREM</p> <p>IPSUM</p></body></html>
* */
Esse tratamento correto é válido para qualquer outra tag.
Obtendo dados com navegação por elementos
Uma das maneiras de obtenção de dados é por meio da navegação da estrutura que passou pelo parse HTML. Igualmente fazemos no código JavaScript puro, quando não temos uma library como o jQuery ou o AngularJS.
Segue um exemplo:
/* NOSSO OBJETIVO É ACESSAR O CONTEÚDO DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p>CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
String conteudoP = doc.getElementsByTag("p").get(0).text();
Simples, certo?
Não mesmo! Para estruturas mais complexas (acredite, a maioria dos códigos HTML são muito mais complexos do que o exemplo anterior) o número de métodos encadeados seria algo a se pensar, isso para uma boa leitura de código.
Caso você ainda não conheça o modelo de seletores CSS, então a melhor maneira de acessar conteúdos, tags e atributos é como apresentado anteriormente.
Mas eu fortemente lhe encorajo a aprender ao menos a base de trabalho com o jQuery, pois assim a busca por dados será muito mais simples, incluindo a alteração desses.
Abaixo um exemplo de como acessar atributos via navegação DOM:
/* NOSSO OBJETIVO É ACESSAR O ATRIBUTO TITLE DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p title=\"TESTE\">CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
String titleAtributo = doc.getElementsByTag("p").get(0).attr("title");
Assim vamos a maneira de navegação via seletores CSS.
Obtendo dados com navegação por seletores
A seguir vamos a um modo de acesso de dados muito mais simples do que a navegação via métodos DOM, utilizando seletores CSS / jQuery.
Segue o mesmo primeiro código da seção anterior, porém com busca via seletor:
/* NOSSO OBJETIVO É ACESSAR O CONTEÚDO DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p>CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
String conteudoP = doc.select("p:eq(0)").text();
A princípio não há uma diferença considerável. Apenas a princípio. Pois em estruturas HTML mais complexas a simplicidade da seleção de objetos se mantém.
Caso ainda não conheça seletores CSS ou jQuery, não se engane em achar que essa será uma curva de aprendizagem longa, pois não vai.
CSS é tão simples quanto o HTML, obviamente que é possível sim ter coisas complexas onde somente profissionais da linguagem saberão trabalhar, mas para uso no JSOUP a base é o suficiente. Digo o mesmo para o estudo do jQuery.
A seguir o mesmo código da seção anterior, o código de acesso ao atributo title de <p>, porém com seletores CSS:
/* NOSSO OBJETIVO É ACESSAR O ATRIBUTO TITLE DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p title=\"TESTE\">CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
String titleAtributo = doc.select("p:eq(0)").attr("title");
Ainda simples, certo? Para todos os modelos de acesso que trabalhamos aqui, incluindo os modelos de navegação DOM, para todos estes é possível também realizar a modificação dos dados, assunto da próxima seção.
Atualizando dados
Como anteriormente, o processo de atualização e atribuição de valores as tags da estrutura HTML é também bem simples.
Segue código de atualização via navegação DOM:
/* NOSSO OBJETIVO É ATUALIZAR O CONTEÚDO DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p>CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
doc.getElementsByTag("p").get(0).text("conteúdo atualizado");
String conteudoP = doc.getElementsByTag("p").get(0).text();
Agora a atualização do atributo title de <p> via navegação por seletores CSS:
/* NOSSO OBJETIVO É ATUALIZAR O ATRIBUTO TITLE DA TAG <p> */
String html = "<html><head><title>TÍTULO</title></head><body><p title=\"TESTE\">CONTEÚDO</p></body></html>";
Document doc = Jsoup.parse(html);
doc.select("p:eq(0)").attr("title", "conteúdo atualizado");
String titleAtributo = doc.select("p:eq(0)").attr("title");
Quando trabalhamos por um tempo com libraries XML parse, não podemos esperar simplicidade em libraries HTML parse, mas caso precise de conteúdo que está disponível somente em HTML, ou seja, não há ao menos uma API JSON, o JSOUP facilita em muito as coisas para nós, developers Android.
Projeto de exemplo, Android
Nosso projeto Android dessa vez é sim um projeto real que ao final do artigo você poderá coloca-lo em sua conta na Google Play Store e, se vincula-lo a uma API de anúncios, iniciar a monetização mobile.
O domínio do problema é futebol, mais precisamente, a apresentação de placares de alguns jogos que estão ocorrendo no dia, alguns que finalizaram recentemente e alguns que vão ocorrer. Ok, esses últimos não têm placares, eu sei, mas também vão estar presentes no aplicativo.
Caso queira realizar o download do projeto e já abri-lo em seu ambiente de desenvolvimento, vá direto ao GitHub dele em: https://github.com/viniciusthiengo/super-placar-jsoup.
Vamos seguir com a apresentação completa do código, porém inicialmente utilizando dados Mock, ou seja, dados locais em nosso aplicativo.
Depois vamos a implementação do JSOUP para obtermos os dados da fonte desejada, dados dinâmicos do site Super Placar.
Na seção de implementação do JSOUP vou falar mais sobre o porquê da escolha desse site.
Logo, continue criando um novo projeto no Android Studio, um projeto com uma "Empty Activity" e o seguinte nome: Super Placar.
Ao final da implementação teremos o seguinte aplicativo:
E a seguinte estrutura no projeto Android:
Vamos as configurações iniciais do projeto.
Configurações Gradle
A seguir a configuração do Gradle Project Level, ou build.gradle (Project: SuperPlacar):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
No Gradle Project Level mantivemos a configuração padrão, lhe adianto que não voltaremos a essa versão, ela permanecerá assim.
Note que caso você esteja com um Gradle mais atual, mantenha utilizando ele, o projeto deve funcionar sem problemas.
A seguir o Gradle App Level, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.superplacar"
minSdkVersion 10
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.1.1'
testCompile 'junit:junit:4.12'
/* PARA USAR O RECYCLERVIEW */
compile 'com.android.support:design:25.1.1'
/* CARREGAR IMAGENS REMOTAS */
compile 'com.squareup.picasso:picasso:2.5.2'
}
Para essa versão nós adicionamos algumas libraries, respectivamente para trabalho com o RecyclerView e trabalho com o carregamento de imagens remotas, para esse último caso adicionamos a library Picasso.
Vamos voltar ao Gradle App Level para adicionarmos a referência a library JSOUP. E sobre o Gradle mais atual, para o App Level o pensamento é o mesmo apresentado para o Project Level.
Configurações AndroidManifest
A configuração inicial do AndroidManifest.xml é bem simples, exatamente como ela é em um novo projeto no Android Studio:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.superplacar">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Configurações de estilo
Para os arquivos de configuração de estilo, adicionamos apenas algumas cores como cores padrões do tema e também adicionamos uma imagem de background para que não seja apresentada uma tela preta assim que o aplicativo é aberto.
Vamos iniciar com o XML de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#607D8B</color>
<color name="colorPrimaryDark">#455A64</color>
<color name="colorAccent">#4CAF50</color>
<color name="colorStatusLabel">#999999</color>
<color name="colorBackground">#303030</color>
</resources>
Agora o arquivo XML de String, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Super Placar</string>
</resources>
E por fim o arquivo XML de definição de tema, /res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Lembrando que para ter acesso as imagens você terá de entrar no GitHub do projeto.
Classes de domínio
Nossas classes de domínio são bem simples. Essas vão representar a estrutura a seguir:
A classe Jogo contém todos os dados de item da lista que é utilizada na atividade principal (a imagem anterior é a imagem de um item).
A classe Time contém todos os dados de um lado. São dois times por objeto Jogo, dois lados.
Cada Time tem uma lista de gols, mesmo que vazia quando esse ainda não fez algum. Cada item da lista de gols é representado por um objeto do tipo Gol, esse contém o tempo do gol e o nome do jogador.
Vamos iniciar com o código de Gol:
public class Gol implements Parcelable {
private String nome;
private String time;
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.nome);
dest.writeString(this.time);
}
public Gol() {
}
protected Gol(Parcel in) {
this.nome = in.readString();
this.time = in.readString();
}
public static final Parcelable.Creator<Gol> CREATOR = new Parcelable.Creator<Gol>() {
@Override
public Gol createFromParcel(Parcel source) {
return new Gol(source);
}
@Override
public Gol[] newArray(int size) {
return new Gol[size];
}
};
}
Estamos utilizando o Parcelable para evitar o recarregamento de novos objetos quando a atividade principal da aplicação é reconstruída, digo, o Parcelable fará parte da lógica de negócio que permite que essa otimização ocorra. Todas as classes de domínio implementam o Parcelable.
Segue código da classe Time:
public class Time implements Parcelable {
private String nome;
private String imagemUrl;
private int gols;
private List<Gol> golsLista;
public Time(){
golsLista = new ArrayList<>();
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getImagemUrl() {
return imagemUrl;
}
public void setImagemUrl(String imagemUrl) {
this.imagemUrl = imagemUrl;
}
public int getGols() {
return gols;
}
public void setGols(int gols) {
this.gols = gols;
}
public List<Gol> getGolsLista() {
return golsLista;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.nome);
dest.writeString(this.imagemUrl);
dest.writeInt(this.gols);
dest.writeTypedList(this.golsLista);
}
protected Time(Parcel in) {
this.nome = in.readString();
this.imagemUrl = in.readString();
this.gols = in.readInt();
this.golsLista = in.createTypedArrayList(Gol.CREATOR);
}
public static final Parcelable.Creator<Time> CREATOR = new Parcelable.Creator<Time>() {
@Override
public Time createFromParcel(Parcel source) {
return new Time(source);
}
@Override
public Time[] newArray(int size) {
return new Time[size];
}
};
}
E por fim o código da classe Jogo:
public class Jogo implements Parcelable {
public static final String JOGOS_KEY = "jogos_key";
private Time time1;
private Time time2;
private String status;
private String inicio;
public Time getTime1() {
return time1;
}
public void setTime1(Time time1) {
this.time1 = time1;
}
public Time getTime2() {
return time2;
}
public void setTime2(Time time2) {
this.time2 = time2;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getInicio() {
return inicio;
}
public void setInicio(String inicio) {
this.inicio = inicio;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(this.time1, flags);
dest.writeParcelable(this.time2, flags);
dest.writeString(this.status);
dest.writeString(this.inicio);
}
public Jogo() {
}
protected Jogo(Parcel in) {
this.time1 = in.readParcelable(Time.class.getClassLoader());
this.time2 = in.readParcelable(Time.class.getClassLoader());
this.status = in.readString();
this.inicio = in.readString();
}
public static final Parcelable.Creator<Jogo> CREATOR = new Parcelable.Creator<Jogo>() {
@Override
public Jogo createFromParcel(Parcel source) {
return new Jogo(source);
}
@Override
public Jogo[] newArray(int size) {
return new Jogo[size];
}
};
}
A constante JOGOS_KEY nós vamos utilizar junto ao onSaveInstanceState() para mantermos a mesma lista de objetos depois da construção de uma nova atividade principal.
Mockdata, dados de teste
Utilizar dados mock (simulado) é uma prática comum no desenvolvimento de software.
Já sabemos como será o projeto, já temos acesso aos diagramas dele, logo, podemos iniciar o desenvolvimento com dados locais e assim seguirmos para a conexão de busca de dados na fonte real. Aqui em nosso projeto a origem real dos dados será a home page de um Web site de partidas de futebol.
Apesar de ser algo opcional, recomendo que você utilize dados mock ao menos neste projeto para entender como essa estratégia de desenvolvimento funciona.
Note que não há um padrão na construção da estrutra mock. A seguir a classe que estamos utilizando como fonte de dados mock, classe Mock:
public class Mock {
public static List<Gol> gerarGols(int qtd ){
String[] times = {"16'1T", "35'1T", "01'2T", "21'2T"};
String[] nomes = {
"Fernando",
"Michael",
"Léo Castro",
"João Paulo"
};
List<Gol> gols = new ArrayList<>();
for( int i = 0; i < qtd; i++ ){
int randomPos = (int) (Math.random() * 4);
Gol g = new Gol();
g.setTime( times[randomPos] );
g.setNome( nomes[randomPos] );
gols.add( g );
}
return gols;
}
public static Time gerarTime(int posicao ){
String[] nomes = {"Rio Claro", "São Caetano", "S. J. Campos", "Nacional-SP"};
String[] imagens = {
"http://www.superplacar.com.br/images/escudos/f1eab3ac03d333dc76278b2f7989bace-68.png",
"http://www.superplacar.com.br/images/escudos/173fb38f10e9a24e7cc665e513575bf2-68.png",
"http://www.superplacar.com.br/images/escudos/a4cd88615deb2decbe7515b74849bee9-68.png",
"http://www.superplacar.com.br/images/escudos/42ecf680e39db12f2ba513263694d1bc-68.PNG"
};
int[] gols = {0, 2, 1, 0};
Time time = new Time();
time.setNome( nomes[ posicao ] );
time.setImagemUrl( imagens[ posicao ] );
time.setGols( gols[ posicao ] );
time.getGolsLista().addAll( gerarGols( gols[ posicao ] ) );
return time;
}
public static Jogo gerarJogo(int posicao ){
String[] status = {"Em andamento", "Em breve", "Encerrado"};
String[] inicios = {"16:55", "19:00", "20:00"};
Jogo jogo = new Jogo();
jogo.setTime1( gerarTime( posicao ) );
jogo.setTime2( gerarTime( posicao + 1 ) );
jogo.setStatus( status[posicao] );
jogo.setInicio( inicios[posicao] );
return jogo;
}
public static List<Jogo> gerarJogos(){
List<Jogo> jogos = new ArrayList<>();
jogos.add( gerarJogo(0) );
jogos.add( gerarJogo(2) );
return jogos;
}
}
Algumas criações de objetos são randômicas, outras não. Como falei, não há uma série de regras quanto a criação da estrutura mock, somente que os dados mock devem representar fielmente os dados quando vindos da fonte real.
Classes adaptadoras
Você deve estar se perguntando: classes adaptadoras? Não é somente uma? Pois há somente uma lista.
Na verdade, em cada item da lista há outras duas listas, listas de jogadores que fizeram gols. Há uma lista dessa abaixo de cada nome de time. Mesmo quando não há gol algum a lista estará lá, vazia.
Vamos iniciar com a lista de jogadores que fizeram gols, mais precisamente, com os layouts desse primeiro adapter, GolsAdapter.
Os layouts?
Sim, são dois. Um para o time do lado esquerdo e outro para o time visitante, o do lado direito. Assim conseguimos manter sempre os tempos dos gols no meio do item e os nomes dos jogadores nas bordas.
Segue código XML de /layout/item_gol_left.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_nome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="@color/colorStatusLabel"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/tv_nome"
android:layout_toRightOf="@+id/tv_nome"
android:textColor="@color/colorStatusLabel"
android:textSize="13sp"
android:textStyle="bold" />
</RelativeLayout>
E assim o código XML de /layout/item_gol_right.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_nome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:textColor="@color/colorStatusLabel"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/tv_nome"
android:layout_toStartOf="@+id/tv_nome"
android:textColor="@color/colorStatusLabel"
android:textSize="13sp"
android:textStyle="bold" />
</RelativeLayout>
Ambos os layouts anteriores têm a seguinte estrutura:
Assim podemos seguir ao código Java de GolsAdapter:
public class GolsAdapter extends RecyclerView.Adapter<GolsAdapter.ViewHolder> {
private Context context;
private int idLayout;
private List<Gol> gols;
class ViewHolder extends RecyclerView.ViewHolder{
TextView tvTime;
TextView tvNome;
ViewHolder(View itemView) {
super(itemView);
tvTime = (TextView) itemView.findViewById(R.id.tv_time);
tvNome = (TextView) itemView.findViewById(R.id.tv_nome);
}
private void setData( Gol gol ){
tvTime.setText( gol.getTime() );
tvNome.setText( gol.getNome() );
}
}
public GolsAdapter(Context context, int idLayout){
this.context = context;
this.idLayout = idLayout;
this.gols = new ArrayList<>();
}
@Override
public GolsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater
.from( context )
.inflate( idLayout, parent, false );
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData( gols.get( position ) );
}
@Override
public int getItemCount() {
return gols.size();
}
public void setGols( List<Gol> gols ){
/* PARA NÃO CRIARMOS UM NOVO OBJETO DE LISTA, UTILIZAMOS clear() e addAll() */
this.gols.clear();
this.gols.addAll( gols );
/* ATUALIZANDO A LISTA DE GOLS NA TELA */
notifyDataSetChanged();
}
}
Código bem simples, sem lógica condicional, somente atribuição de valores. Note que com esse adapter já iniciamos a lista de gols com zero objetos, isso, pois esse é o estado padrão para cada time em um jogo em nosso domínio do problema.
Agora vamos aos códigos do adapter que contém listas de gols, o JogosAdapter. Esse têm somente um layout e vamos iniciar por ele.
Segue código XML de /layout/item_jogo.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="20dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginTop="-2dp"
android:gravity="center"
android:textColor="@color/colorStatusLabel"
android:textSize="11sp" />
<ImageView
android:id="@+id/iv_time_1"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_nome_time_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_time_1"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLength="16"
android:maxLines="1"
android:textSize="16sp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_gols_time_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_nome_time_1"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/tv_gols_time_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_toLeftOf="@+id/vs"
android:layout_toStartOf="@+id/vs"
android:textSize="28sp" />
<TextView
android:id="@+id/vs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="x"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_gols_time_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_toEndOf="@+id/vs"
android:layout_toRightOf="@+id/vs"
android:textSize="28sp" />
<ImageView
android:id="@+id/iv_time_2"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_nome_time_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/iv_time_2"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLength="16"
android:maxLines="1"
android:textSize="16sp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_gols_time_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_nome_time_2"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp" />
</RelativeLayout>
Código um pouco maior que os layouts anteriores, porém ainda não complexo. Segue diagrama de item_jogo.xml:
Assim o código Java de JogosAdapter:
public class JogosAdapter extends RecyclerView.Adapter<JogosAdapter.ViewHolder> {
private Context context;
private List<Jogo> jogos;
class ViewHolder extends RecyclerView.ViewHolder{
TextView tvStatus;
ImageView ivTime1;
TextView tvNomeTime1;
TextView tvGolsTime1;
RecyclerView rvTime1;
ImageView ivTime2;
TextView tvGolsTime2;
TextView tvNomeTime2;
RecyclerView rvTime2;
ViewHolder(View itemView) {
super(itemView);
tvStatus = (TextView) itemView.findViewById(R.id.tv_status);
ivTime1 = (ImageView) itemView.findViewById(R.id.iv_time_1);
tvNomeTime1 = (TextView) itemView.findViewById(R.id.tv_nome_time_1);
tvGolsTime1 = (TextView) itemView.findViewById(R.id.tv_gols_time_1);
rvTime1 = initRecyclerView( R.id.rv_gols_time_1, R.layout.item_gol_left );
ivTime2 = (ImageView) itemView.findViewById(R.id.iv_time_2);
tvGolsTime2 = (TextView) itemView.findViewById(R.id.tv_gols_time_2);
tvNomeTime2 = (TextView) itemView.findViewById(R.id.tv_nome_time_2);
rvTime2 = initRecyclerView( R.id.rv_gols_time_2, R.layout.item_gol_right );
}
private RecyclerView initRecyclerView( int rvId, int idLayout ){
RecyclerView rv = (RecyclerView) itemView.findViewById( rvId );
LinearLayoutManager mLayoutManager = new LinearLayoutManager( context );
mLayoutManager.setAutoMeasureEnabled(true);
rv.setLayoutManager(mLayoutManager);
rv.setAdapter( new GolsAdapter(context, idLayout) );
return rv;
}
private void setData( Jogo jogo ){
tvStatus.setText(
Html.fromHtml( "<b>"+jogo.getStatus()+"</b> ("+jogo.getInicio()+")" ) );
Picasso.with( context )
.load( jogo.getTime1().getImagemUrl() )
.into( ivTime1 );
tvNomeTime1.setText( String.valueOf( jogo.getTime1().getNome() ) );
tvGolsTime1.setText( String.valueOf( jogo.getTime1().getGols() ) );
updateRecyclerView( rvTime1, jogo.getTime1().getGolsLista() );
Picasso.with( context )
.load( jogo.getTime2().getImagemUrl() )
.into( ivTime2 );
tvNomeTime2.setText( String.valueOf( jogo.getTime2().getNome() ) );
tvGolsTime2.setText( String.valueOf( jogo.getTime2().getGols() ) );
updateRecyclerView( rvTime2, jogo.getTime2().getGolsLista() );
}
private void updateRecyclerView( RecyclerView rv, List<Gol> gols ){
GolsAdapter adapter = (GolsAdapter) rv.getAdapter();
adapter.setGols( gols );
}
}
public JogosAdapter(Context context, List<Jogo> jogos){
this.context = context;
this.jogos = jogos;
}
@Override
public JogosAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater
.from( context )
.inflate(R.layout.item_jogo, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData( jogos.get( position ) );
}
@Override
public int getItemCount() {
return jogos.size();
}
}
Como no adapter GolsAdapter, novamente somente código de atribuição. Note o código de inicialização de RecyclerView:
...
private RecyclerView initRecyclerView( int rvId, int idLayout ){
RecyclerView rv = (RecyclerView) itemView.findViewById( rvId );
LinearLayoutManager mLayoutManager = new LinearLayoutManager( context );
mLayoutManager.setAutoMeasureEnabled(true);
rv.setLayoutManager(mLayoutManager);
rv.setAdapter( new GolsAdapter(context, idLayout) );
return rv;
}
...
Este está encapsulado devido a ser necessário em ambos os casos, para o RecyclerView da esquerda e para o da direita. É nele também que definimos o layout que será utilizado para a lista de gols.
Recomendo essa prática, trabalhar com RecyclerView interno a outro, caso seja necessário o uso de lista, principalmente quando não se sabe o tamanho da lista, quando esse não é fixo. Essa é uma prática que mantém os códigos bem separados, ao contrário do que seria se trabalhando com outras Views e o método setVisibility() de cada uma delas.
Assim podemos prosseguir a última parte do código inicial, a atividade principal do projeto.
Atividade principal
A atividade principal é bem simples, o trecho grande de código ficou mesmo por conta das classes adaptadoras.
Vamos iniciar com o layout, segue código XML de /layout/activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
tools:context="br.com.thiengo.superplacar.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_jogos"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Então o diagrama do layout anterior:
Assim o código Java inicial da MainActivity:
public class MainActivity extends AppCompatActivity {
private ArrayList<Jogo> jogos;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if( savedInstanceState != null ){
jogos = savedInstanceState.getParcelableArrayList(Jogo.JOGOS_KEY);
}
else{
jogos = Mock.gerarJogos();
}
initViews();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(Jogo.JOGOS_KEY, jogos);
super.onSaveInstanceState(outState);
}
private void initViews() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_jogos);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager( this );
recyclerView.setLayoutManager(mLayoutManager);
DividerItemDecoration divider = new DividerItemDecoration(
this,
mLayoutManager.getOrientation() );
recyclerView.addItemDecoration( divider );
JogosAdapter adapter = new JogosAdapter( this, jogos );
recyclerView.setAdapter( adapter );
}
}
Digo inicial, pois voltaremos a esta classe para realizar algumas atualizações, principalmente a remoção da referência de trabalho com dados mock.
Com o que construímos até aqui, quando executando o aplicativo, teremos uma tela similar a tela a seguir:
Assim podemos iniciar o trabalho com a library JSOUP.
Atualização do aplicativo para requisições e processamentos JSOUP
Nosso objetivo: trabalhar com dados reais de jogos de futebol, porém sem termos de criar toda a estrutura back-end para gerenciamento desses dados.
Com isso devemos primeiro buscar alguma API online que permita a obtenção desses dados, nos formatos JSON ou XML, via WebService.
Aqui encontramos um site que têm a atualização em tempo real dos principais jogos de futebol que estão acontecendo no Brasil e no mundo. Porém esse site não tem nem mesmo um aplicativo Android.
A seguir a interface que temos em Super Placar:
Vamos utilizar esse site em nosso aplicativo, digo, os dados HTML dele, porém sem um WebView.
Com o JSOUP vamos extrair, de tempos em tempos, os dados que nos importam e então oferecer aos nossos usuários uma interface mais limpa e intuitiva em um dispositivo mobile.
Entendendo a estrutura HTML do site
Uma das desvantagens de trabalho com o parse HTML é que temos de saber exatamente como é a estrutura HTML que estaremos trabalhando para depois construirmos o código que vai utilizar os dados do parse.
A seguir o diagrama que mostra quais os itens da home page de Super Placar que estaremos acessando para extração de dados:
Não coloquei aqui o diagrama colorido como na definição dos layouts XML, pois, acredite, o HTML desse site está exageradamente grande.
A seguir vamos a listagem dos seletores HTML que poderemos utilizar para acessar os conteúdos destacados anteriormente.
Começando pelos dois dados gerais, em vermelho:
- Todos os inícios de jogos, os tempos: "div.time-status span.time";
- Todos os status dos jogos: "div.time-status span.status".
Agora como acessaremos todos os dados dos times um, ou times que estão jogando em casa, destaques em verde:
- Todos os nomes: "div.team1 span.team1-name";
- Todas as imagens: "div.team1 img" (aqui vamos precisar mesmo é do conteúdo do atributo src);
- Todos os placares de gols: "div.team1 span.team1-score";
- Todos os detalhes de gols: "div.team1 ul.goal-players li .name" e "div.team1 ul.goal-players li .time".
Para detalhes dos times dois, ou visitantes, o procedimento é o mesmo, mudando somente o índice de 1 para 2. Segue:
- Todos os nomes: "div.team2 span.team2-name";
- Todas as imagens: "div.team2 img";
- Todos os placares de gols: "div.team2 span.team2-score";
- Todos os detalhes de gols: "div.team2 ul.goal-players li .name" e "div.team2 ul.goal-players li .time".
Para debugar o código HTML do site eu utilizei a ferramenta de desenvolvedor do Google Chrome:
Para ativa-la basta ir, no Chrome, em "Menu" > "More Tools" > "Developer Tools" e então clicar na pequena seta mais a esquerda no topo da janela que se abriu.
Com isso podemos seguir com a implementação JSOUP.
Atualização Gradle
Atualize o Gradle App Level, ou build.gradle (Modul: app). Insira a referência a library JSOUP:
apply plugin: 'com.android.application'
...
dependencies {
...
/* PARA USAR O JSOUP */
compile 'org.jsoup:jsoup:1.10.2'
}
Logo depois sincronize o projeto.
Atualização AndroidManifest
No AndroidManifest.xml somente adicione a permissão de conexão com a Internet:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.superplacar">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
Classe de requisição de dados
No pacote extras, onde está a classe Mock, vamos adicionar uma nova classe, uma que herda de AsyncTask, pois ela será responsável por utilizar o código de conexão de JSOUP, ou seja, precisaremos de trabalhar em uma Thread secundária.
Segue código inicial da classe SuperPlacarRequest:
public class SuperPlacarRequest extends AsyncTask<Void, Void, List<Jogo>> {
private WeakReference<MainActivity> activity;
public SuperPlacarRequest( MainActivity activity ){
this.activity = new WeakReference<>( activity );
}
@Override
protected List<Jogo> doInBackground(Void... voids) {
Document html = null;
List<Jogo> jogos = new ArrayList<>();
try {
html = Jsoup.connect("http://www.superplacar.com.br/").get();
}
catch (IOException e) {
e.printStackTrace();
}
return jogos;
}
@Override
protected void onPostExecute(List<Jogo> jogos) {
super.onPostExecute( jogos );
}
}
Assim já temos algo com o JSOUP. Note que vamos trabalhar com a atividade principal, por meio de uma referência fraca, WeakReference, para evitar ao máximo o vazamento de memória. Isso, pois o ciclo de vida de SuperPlacarRequest é diferente do ciclo de MainActivity.
Depois que temos a variável html com o conteúdo completo, podemos iniciar os códigos seletores, isso para caminharmos corretamente até os dados que precisamos.
Logo abaixo de Jsoup.connect() adicione:
...
Elements time = html.select("div.time-status span.time");
Elements status = html.select("div.time-status span.status");
Elements times1 = html.select("div.team1");
Elements times2 = html.select("div.team2");
...
Com isso precisamos de um loop, pois nós temos todos os dados de todos os jogos e precisamos na verdade de acessar cada jogo de forma individual para construirmos os objetos corretamente.
Como sabemos que na busca pelos dados do time 1 e do time 2 somente muda o índice nos seletores, podemos seguramente encapsular a criação de um novo objeto Time com dados de seletores.
Logo depois de doInBackground() adicione o seguinte método:
...
private Time getTime( Element timeTag, boolean ehCasa ){
int indice = ehCasa ? 1 : 2;
Time time = new Time();
time.setNome( timeTag.select("span.team"+indice+"-name").text() );
time.setImagemUrl( timeTag.select("img").attr("src") );
String golsString = timeTag.select("span.team"+indice+"-score").text();
int gols = golsString.isEmpty() ? 0 : Integer.parseInt( golsString );
time.setGols( gols );
time.getGolsLista().addAll( getGolsLista( timeTag ) );
return time;
}
...
Note que o segundo parâmetro, ehCasa, é para indicar se é para utilizarmos os dados do time da casa ou do time visitante, por isso o condicional para a variável indice.
Note também que já adicionamos a invocação ao método getGolsLista(). Este ainda devemos construir, pois como acontece com a construção de um objeto do tipo Time, a construção da lista de objetos do tipo Gol é similar para o time 1 e time 2.
Logo depois do método getTime() adicione o seguinte método:
...
private List<Gol> getGolsLista( Element timeTag ){
Elements golsLista = timeTag.select("ul.goal-players li");
List<Gol> gols = new ArrayList<>();
for( Element g : golsLista ){
Gol gol = new Gol();
gol.setNome( g.select(".name").text() );
gol.setTime( g.select(".time").text() );
gols.add( gol );
}
return gols;
}
...
Dessa vez não temos índices nos seletores.
Por que temos de ter um inteiro para os gols e ao mesmo tempo uma lista com os detalhes dos gols? Por que não utilizamos somente a lista?
Bem observado. Isso é devido ao trabalho no site Super Placar. A atualização do "gol placar" vem primeiro que a atualização do "gol em detalhes". Acredite, estou falando de um delay de minutos. Com isso devemos sim manter a coleta também do gol placar, pois ele é mais consistente.
Assim, voltando ao doInBackground(), adicione os códigos destacados a seguir:
...
@Override
protected List<Jogo> doInBackground(Void... voids) {
Document html = null;
List<Jogo> jogos = new ArrayList<>();
try {
html = Jsoup.connect("http://www.superplacar.com.br/").get();
Elements time = html.select("div.time-status span.time");
Elements status = html.select("div.time-status span.status");
Elements times1 = html.select("div.team1");
Elements times2 = html.select("div.team2");
for( int i = 0; i < time.size(); i++ ){
Time time1 = getTime( times1.get(i), true );
Time time2 = getTime( times2.get(i), false );
Jogo jogo = new Jogo();
jogo.setInicio( time.get(i).text() );
jogo.setStatus( status.get(i).text() );
jogo.setTime1( time1 );
jogo.setTime2( time2 );
jogos.add( jogo );
}
}
catch (IOException e) {
e.printStackTrace();
}
return jogos;
}
...
Intuitivo, certo? Somente estamos construindo os objetos que vão substituir a lista de dados mock.
Ainda temos o método onPostExecute(), este precisa ter o código necessário para atualizar a lista de jogos na MainActivity. Segue:
...
@Override
protected void onPostExecute(List<Jogo> jogos) {
super.onPostExecute( jogos );
if( activity.get() != null ){
activity.get().updateLista( jogos );
}
}
...
Precisamos adicionar o método updateLista() na MainActivity. Na seção de atualização dessa atividade nós vamos também adicionar esse método.
Agora devemos criar a classe que vai nos permitir mantermos a atualização dos dados de jogos, isso sem que o usuário precise ter alguma ação no aplicativo, somente deixa-lo aberto.
Classe de repetição de requisição
Ainda no pacote extras crie uma nova classe, esta terá a execução principal dentro de uma Thread secundária.
Segue código de Worker:
public class Worker extends Thread {
private WeakReference<MainActivity> activity;
public Worker( MainActivity activity ){
this.activity = new WeakReference<>( activity );
}
@Override
public void run() {
super.run();
while( activity.get() != null ){
SystemClock.sleep(20000);
new SuperPlacarRequest( activity.get() ).execute();
}
}
}
Coloquei o intervalo de requisição de 20 em 20 segundos, mas você pode ajustar como bem entender. Somente note que com a lógica de negócio inicial que estaremos utilizando aqui, cada nova invocação a instância de SuperPlacarRequest cria novamente todos os objetos necessários em lista.
Esse comportamento é ruim em aplicativos Android, pois pode ser que somente um dado de um único item tenha mudado, as vezes até mesmo nenhum dado pode ter sido alterado. Mas mesmo assim todos os objetos serão reconstruídos.
Para amenizar o problema, defini que para atualização de resultados de jogos de futebol, o intervalo de 20 segundos atende muito bem e não exausta o aplicativo acionando com muita frequência o coletor de lixo do Java.
Se preferir um tempo menor, recomendo que também trabalhe mais a lógica, digo, somente atualize os itens que têm de ser atualizados. Fazer isso com o RecyclerView é bem tranquilo.
Veja que como acontece em SuperPlacarRequest onde temos um ciclo de vida distinto do da MainActivity, em Worker precisamos da atividade principal, porém também temos um ciclo de vida de objeto diferente do ciclo da MainActivity, logo, utilizamos novamente uma referência fraca.
Para finalizar, vamos as atualizações da atividade principal.
Atualizações da atividade principal
Temos primeiro que saber que não mais utilizaremos a classe Mock, logo temos de iniciar a variável jogos com uma lista vazia, pois a busca de dados em Super Placar é assíncrona.
Segue em destaque os trechos adicionados:
public class MainActivity extends AppCompatActivity {
private JogosAdapter adapter;
private ArrayList<Jogo> jogos;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if( savedInstanceState != null ){
jogos = savedInstanceState.getParcelableArrayList(Jogo.JOGOS_KEY);
initViews();
retrieveJogosStream();
}
else{
jogos = new ArrayList<>();
initViews();
retrieveJogos();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(Jogo.JOGOS_KEY, jogos);
super.onSaveInstanceState(outState);
}
private void initViews() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_jogos);
recyclerView.setHasFixedSize(true);
LinearLayoutManager mLayoutManager = new LinearLayoutManager( this );
recyclerView.setLayoutManager(mLayoutManager);
DividerItemDecoration divider = new DividerItemDecoration(
this,
mLayoutManager.getOrientation() );
recyclerView.addItemDecoration( divider );
adapter = new JogosAdapter( this, jogos );
recyclerView.setAdapter( adapter );
}
private void retrieveJogos(){
new SuperPlacarRequest(this).execute();
retrieveJogosStream();
}
private void retrieveJogosStream(){
new Worker(this).start();
}
public void updateLista( List<Jogo> j ){
jogos.clear();
jogos.addAll( j );
adapter.notifyDataSetChanged();
}
}
Enfim a apresentação do método updateLista(), método invocado em onPostExecute() de SuperPlacarRequest.
Os métodos retrieveJogos() e retrieveJogosStream() existem para colocarmos maior intuição na leitura do código, pois devido aos conteúdos deles, conteúdos pequenos, poderíamos facilmente utiliza-los, os conteúdos, inline.
Assim podemos iniciar para os testes.
Testes e resultados
No momento dos testes finais para este artigo, a página do Super Placar estava com o seguinte status:
Digo, um único placar de jogo em andamento, dois times franceses.
Executando o aplicativo e dando um scroll down nele, temos:
Campeonato Francês, como no site, "em andamento".
Assim terminamos o trabalho com o JSOUP para construir um aplicativo real. De 20 em 20 segundos o conteúdo será atualizado.
Você poderia melhorar ainda mais o código, colocando, por exemplo, um ProgressBar que indica que o conteúdo está sendo carregado de uma fonte remota.
Para que você possa monetizar, terá de colocar alguma API de anúncios, assinar o aplicativo e disponibiliza-lo na Play Store.
Não se esqueça de se cadastrar na 📫 lista de e-mails do Blog para receber os conteúdos em primeira mão.
Siga também o canal no YouTube.
Vídeo com implementação passo a passo do projeto
Abaixo o vídeo com a implementação passo a passo do JSOUP no projeto do aplicativo Super Placar:
Para acesso ao conteúdo completo do projeto, entre no seguinte GitHub: https://github.com/viniciusthiengo/super-placar-jsoup.
Conclusão
Sabendo da necessidade dos dados de determinado site, ou arquivo local HTML, e então concluindo que não há uma API JSON ou XML para o acesso a esses dados, utilize a API JSOUP, realizando assim o parse HTML.
Dependendo do tipo de aplicativo que você esteja construindo, poderá até mesmo criar a estrutura HTML utilizando o JSOUP.
O principal ponto negativo está em quando se utiliza um site que não é de nossa autoria, digo, o HTML não é mantido por nós. Neste caso é preciso "vigiar" o aplicativo com frequência para saber se os dados ainda estão sendo obtidos sem problemas.
Se identificado problemas na recuperação de dados do código HTML do site, volte a ele, ao site, estude o HTML novamente e altere o aplicativo onde deve ser alterado.
Caso você seja o administrador também do código HTML, não esqueça que mesmo alterando a estrutura dos aplicativos Web e mobile é importante manter a antiga versão HTML da parte Web, mesmo que escondida, para que os usuários continue com a versão antiga do aplicativo funcionando.
Estudar o jQuery, seletores CSS e o cookbook JSOUP vai lhe dar ainda mais sabedoria de como melhor buscar ou criar conteúdos com a API JSOUP.
Então é isso, não encontrada uma outra API, JSON ou XML, inicie com o parse HTML. Não se esqueça de se inscrever 📩 na lista de e-mails do Blog.
Caso tenha alguma dúvida ou sugestão, deixe logo abaixo nos comentários.
Abraço.
Comentários Facebook