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:
- Criar Recyclerview e carregar dados dinanmicamente
- Como carregar mais de um ViewHolder no recyclerview
- Como usar CardView e Picasso
- Mandar e receber dados de uma Activity para outra
- Como retornar dado de uma classe AsyncTask
- Como fazer paginação com Jsoup
- 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”:
- Receber os dados via getExtra do “SplashActivity”
- Expor esses dados (imagem e titulo das noticias) no recyclerview
- Usar a função onLoadMore() da intrface “ILoadMore” para indicar que queremos carregar mais noticias
- 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!