Web Scraping no Android com Jsoup – Tutorial Kotlin – Parte 2

Nessa segunda parte do nosso tutorial de Web Scraping com Jsoup no Android Studio em Kotlin vamos implementar Recyclerview para despor as imagens e títulos das noticias e faremos o carregamento em paginação de mais noticias de forma dinâmica.

O que você vai aprender:

  1. Criar Recyclerview e carregar dados dinanmicamente
  2. Como carregar mais de um ViewHolder no recyclerview
  3. Como usar CardView e Picasso
  4. Mandar e receber dados de uma Activity para outra
  5. Como retornar dado de uma classe AsyncTask
  6. Como fazer paginação com Jsoup
  7. Como usar Interfaces para evitar dependência entre classes

Vamos colocar o recyclerview no layout do MainActivity, o “activity_main“:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/colorPrimaryDark">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="8dp"
        android:paddingBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Vamos iniciar a criação do item do nosso recyclerview. Começando a criar o drawable “gradient_card” para o background do CardView do item do recyclerview:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <gradient
        android:startColor="#33000000"
        android:endColor="@android:color/transparent"
        android:angle="90"/>

</shape>

E o drawable gradient_title para o background do titulo do item:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <gradient
        android:startColor="#000000"
        android:endColor="#754E4D4D"
        android:angle="90"/>

</shape>

Agora sim criaremos o layout “item_news” para criar o item:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="250dp"
        style="@style/CardView.Light"
        app:cardCornerRadius="8dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_margin="8dp">

        <ImageView
            android:id="@+id/image_card"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/gradient_card"/>

        <TextView
            android:id="@+id/txt_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            style="@style/TextAppearance.AppCompat.Headline"
            android:background="@drawable/gradient_title"
            android:text="Title"
            android:textColor="#ffffff"
            android:padding="8dp"
            android:textSize="18sp"
            android:textStyle="bold"/>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

Como queremos fazer um carregamento dinâmico no recyclerview vamos criar o layout “item_loader” com um progressbar que aparecerá quando o usuário arrastar todos os itens iniciais carregados, indicando que mais itens estão sendo carregados:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Agora podemos iniciar a programação do carregamento dos dados no recyclerview. Crie a interface “ILoadMore” que ajudará no controle do carregamento de mais noticias:

interface ILoadMore {
    fun onLoadMore()
}

Em seguida crie a classe “NewsViewHolder” que carregará o layout “item_news” no recyclerview:

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.item_news.view.*

class NewsViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {

    val img_news: ImageView = itemView.image_card
    val txt_title: TextView = itemView.txt_title

    fun bindView(news: News){
        txt_title.text = news.title
        Picasso.get().load(news.image).into(img_news)
    }

}

E a classe “LoadViewHolder” que carregará o layout “item_loader” no recyclerview:

import android.view.View
import android.widget.ProgressBar
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_loader.view.*

class LoadViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {

    val progressbar: ProgressBar = itemView.progressBar

    fun bindView(){
        progressbar.isIndeterminate = true
    }

}

E agora a classe “NewsAdapter” que adaptará nossos objetos “News” no “RecyclerView”:

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class NewAdapter(recyclerView: RecyclerView, var activity: Activity, var news: MutableList<News?>):
        RecyclerView.Adapter<RecyclerView.ViewHolder>()
{
   val VIEW_TYPE_ITEM = 0
    val VIEW_TYPE_LOADING = 1
    val visibleThreshold = 5
    var loadMore: ILoadMore? = null
    var isLoading: Boolean = false
    var lastVisibleItem: Int = 0
    var totalItemCount: Int = 0

    init {
        val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener(){
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                totalItemCount = linearLayoutManager.itemCount
                lastVisibleItem = linearLayoutManager.findLastCompletelyVisibleItemPosition()
                if (!isLoading && totalItemCount <= (lastVisibleItem + visibleThreshold)){
                    if (loadMore != null){
                        loadMore!!.onLoadMore()
                    }
                    isLoading = true
                }
            }
        })
    }

    override fun getItemViewType(position: Int): Int {
        if (news[position] == null){
            return VIEW_TYPE_LOADING
        }else{
            return VIEW_TYPE_ITEM
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        var viewHolder: RecyclerView.ViewHolder? = null
        var view: View? = null
        if (viewType == VIEW_TYPE_ITEM){
            view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_news, parent, false)
            viewHolder = NewsViewHolder(view)
        } else if (viewType == VIEW_TYPE_LOADING){
            view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_loader, parent, false)
            viewHolder = LoadViewHolder(view)
        }
        return viewHolder!!
    }

    fun getLoadMore(iLoaded: ILoadMore){
        this.loadMore = iLoaded
    }

    override fun getItemCount(): Int {
        return news.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is NewsViewHolder){
            holder.bindView(news[position]!!)
        } else if (holder is LoadViewHolder){
            holder.bindView()
        }
    }

    fun setLoaded(){
        isLoading = false
    }

}

Agora vamos cuidar da implementação do envio e do recebimentos dados entre as Activities! Crie a interface “IJsoupData” que ajudará retorno de dados na classe AsyncTask:

interface IJsoupData {
    fun getWebData(datas: ArrayList<News>)
}

Já que a classe AsyncTask usada na Activity “SplashActivity” na primeira parte do tutorial é a mesma que precisamos na Activity “MainActivity” vamos reutilizar-la! Crie a classe “LoadNews” e passe todo o código da classe “LoadInitNews” para ela. Já vamos também implementar uma variável do tipo “IJsoupData” para retornar o resultado de nossa consulta Web Scraping e acrescentaremos uma variável no construtor dessa classe para fazer a paginação contida no site do Ministério da Saúde do Brasil :

Para entender com mais detalhes sobre como fazer a paginação veja o tutorial no vídeo do canal Dev MunDi.

import android.os.AsyncTask
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
import java.io.IOException

class LoadNews(var activity: AppCompatActivity?, var page: String): AsyncTask<Void, Void, ArrayList<News>>(){

        private var news: ArrayList<News> = ArrayList()
        private var loadedData = activity as IJsoupData

        override fun doInBackground(vararg params: Void?): ArrayList<News> {
            try {
                val url = "https://www.saude.gov.br/fakenews?$page"
                val doc: Document = Jsoup.connect(url).get()
                //get images inside of the div
                val div: Elements = doc.select("div.tileImage")
                //get titles inside of the H2
                val tagHeading: Elements = doc.select("h2.tileHeadline")

                val size: Int = div.size
                for (index in 0..size){
                    //get image link inside tag "img" with attribute src
                    val imgUrl: String = div.select("img").eq(index).attr("src")
                    //get text title inside tag "a"
                    val title: String = tagHeading.select("a").eq(index).text()
                    //get details news link inside tag "a" with attribute "href"
                    val details: String = tagHeading.select("a").attr("href")

                    Log.i("Result", imgUrl + " " + title + " " + details)
                    news.add(New("https://www.saude.gov.br"+imgUrl, title, details))
                }

            }catch (e: IOException){
                e.printStackTrace()
            }
            return news
        }

        override fun onPostExecute(result: ArrayList<News>?) {
            loadedData.getWebData(result!!)
        }
}

Vamos agora usar a interface “IJsoupData” para receber o retorno da classe “LoadNews” e mandar via putExtra as informações para o “MainActivity” quando o carregamentos assíncrono das primeiras noticias acabar:

import android.content.Intent
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class SplashActivity : AppCompatActivity(), IJsoupData {

    private var loader: AsyncTask<Void, Void, ArrayList<News>>? = null
    private val WEB_PAGE: String = "limitstart=0"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        loader = LoadNews(this, WEB_PAGE)
        loader!!.execute()
    }

    override fun getWebData(datas: ArrayList<News>) {
            val intent = Intent(this, MainActivity::class.java)
            intent.putExtra("NEWS", datas)
            startActivity(intent)
            finish()
    }
}

Não podemos esquecer de extender a classe “News” para Serializable, pois só assim poderemos passar seus objetos via putExtra:

import java.io.Serializable

class News (var image: String, var title: String, var details: String): Serializable

Para finalizarmos só falta nossa Activity “MainActivity”:

  1. Receber os dados via getExtra do “SplashActivity”
  2. Expor esses dados (imagem e titulo das noticias) no recyclerview
  3. Usar a função onLoadMore() da intrface “ILoadMore” para indicar que queremos carregar mais noticias
  4. Usar a função getWebData() da interface “IJsoupData” para receber mais dados na classe AsycnTask “LoadNews” e notificar ao Adapter do recyclerview que há mudanças!
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), ILoadMore, IJsoupData {

    private var news: ArrayList<News>? = ArrayList()
    private var newsLoad: MutableList<News?> = ArrayList()
    lateinit var newsAdapter: NewsAdapter
    private var loader: AsyncTask<Void, Void, ArrayList<News>>? = null
    private var numberPage: Int = 0
    private var WEB_PAGE: String? = null

    override fun onLoadMore() {
        numberPage+=10
        WEB_PAGE = "start=$numberPage"
        loader = LoadNews(this, WEB_PAGE!!)
        loader!!.execute()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        news = intent.getSerializableExtra("NEWS") as ArrayList<News>
        getTenNews(news!!)

        recyclerview.layoutManager = LinearLayoutManager(this)
        newsAdapter = NewsAdapter(recyclerview, this, newsLoad)
        recyclerview.adapter = newsAdapter
        newsAdapter.getLoadMore(this)
    }

    private fun getTenNews(listNews: ArrayList<News>) {
        for (index in 0..9){
            newsLoad.add(listNews[index])
        }
    }

    override fun getWebData(datas: ArrayList<News>) {
        if (newsLoad.size < 50){
            newsLoad.add(null)
            newsAdapter.notifyItemInserted(newsLoad.size-1)

            Handler().postDelayed({
                newsLoad.removeAt(newsLoad.size-1)
                newsAdapter.notifyItemRemoved(newsLoad.size)

                getTenNews(datas)

                newsAdapter.notifyDataSetChanged()
                newsAdapter.setLoaded()

            }, 3000)
        } else {
            Toast.makeText(this, "Load data finish!", Toast.LENGTH_SHORT).show()
        }
    }

}

Pronto!! Agora já carregamos as noticias mais recentes para o recyclerview e podemos carregar noticias mais “antigas” apenas se quisermos!

Caso fique dúvida nos passos desse tutorial você pode assistir ao vídeo do canal Dev Mundi abaixo que contem mais detalhes!

Jsoup Recycler no Android Studio Kotlin

Deixe um comentário