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

Nessa terceira parte do nosso tutorial de Web Scraping com Jsoup no Android Studio em Kotlin vamos envio de dados para outra Activity quando clicarmos em um item do recyclerview e também a carregar mais informações da noticia selecionada de outra pagina web.

O que você vai aprender:

  1. Enviar dados via onClickListener para outra Activity
  2. Como fazer filtragem de tags com Jsoup
  3. Como criar Menu e chama-lo na Activity
  4. Como ativar botão Home As Up
  5. Usar provedor de conteúdo FileProvider para compartilhar imagem com outros apps (como instagram e whatsapp)

Criar a Activity  Empty “DetailsNewsActivity

Agora iremos implementar o envio de dados via onClickListener no NewsAdapter na função “onBindViewHolder” que criamos na parte 2 desse tutorial:

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

        //sending data
        val newsItem = news[position]

        holder.itemView.setOnClickListener {
            val intent = Intent(activity, DetailsNewsActivity::class.java)
            intent.putExtra("IMAGE", newsItem!!.image)
            intent.putExtra("TITLE", newsItem.title)
            intent.putExtra("DETAILS", newsItem.details)
            activity.startActivity(intent)
        }
    }

Crie o drawable “background_shape_details” para personalizarmos o fundo do nosso layout:

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

    <solid
        android:color="@color/colorAccent"/>

    <corners
        android:topLeftRadius="32dp"
        android:topRightRadius="32dp"/>

</shape>

Agora podemos começar a editar o “layout_details_news” do DeitalsNewsActivity:

<?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"
    android:background="@color/colorPrimaryDark"
    tools:context=".DetailsNewsActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:background="@drawable/background_shape_details">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/txt_title_detail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                style="@style/TextAppearance.AppCompat.Headline"
                android:layout_margin="12dp"
                android:padding="4dp"
                android:text="Title"
                android:textSize="18sp"
                android:textStyle="bold"
                app:layout_constraintEnd_toStartOf="parent"
                app:layout_constraintStart_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

            <androidx.cardview.widget.CardView
                android:id="@+id/cardView"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                style="@style/CardView.Light"
                android:layout_marginTop="8dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/txt_title_detail">

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

            </androidx.cardview.widget.CardView>

            <TextView
                android:id="@+id/txt_detail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginTop="12dp"
                android:layout_marginEnd="12dp"
                android:layout_marginBottom="16dp"
                android:hint="Loading..."
                android:padding="4dp"
                android:textSize="16sp"
                app:layout_constraintEnd_toStartOf="@id/cardView"
                app:layout_constraintStart_toEndOf="@id/cardView"
                app:layout_constraintTop_toBottomOf="@id/cardView"/>

        </androidx.constraintlayout.widget.ConstraintLayout>

    </ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

Crie a interface “ILoadDetails” que será responsável por retornar os dados vindos da classe AsyncTask que criaremos mais a frente:

interface ILoadDetails {
    fun getDetails(details: ArrayList<String>)
}

Agora crie a classe AsyncTask “LoadDetailsNews” que receberá o endereço da página que tem o texto completo da notícia onde através do Jsoup filtraremos as tags corretas para pegar apenas o texto e retornaremos o resultado pela interface “ILoadDetails”:

Obs.: Sendo que algumas notícias tem listas (tags “li”) em seu texto (disposto em tags “p”) e outras não, precisamos filtra-las de uma forma que sirva para as duas situações! O que pode ser observado no código abaixo. Mas caso haja dúvida você pode ver com mais detalhes no vídeo tutorial em nossa canal Dev Mundi.

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

class LoadDetailsNews(activity: AppCompatActivity, var urlDetails: String):
    AsyncTask<Void, Void, ArrayList<String>>() {

    private var details: ArrayList<String>? = ArrayList()
    private var loader = activity as ILoadDetails

    override fun doInBackground(vararg params: Void?): ArrayList<String> {
        try{
            val baseUrl = "https://www.saude.gov.br"
            val url = baseUrl+urlDetails

            val doc: Document = Jsoup.connect(url).get()
            val div: Elements = doc.select("div.item-pagenoticias")
            div.select("div").remove()
            //get only the text inside the tags "p" and "li"
            val textComplete = div.select("p, ul > li")

            for (element in textComplete){
                if (element.text() != "")
                    details!!.add(element.text())
            }

        }catch (e: IOException){
            e.printStackTrace()
        }
        return details!!
    }

    override fun onPostExecute(result: ArrayList<String>?) {
        loader.getDetails(result!!)
    }

}

Crie o Vector Asset “ic_share” para o icone do nosso menu:

ic_share

E agora click com o botão direito na pasta res e vá em New -> Android Resource File:

New Android Resource File

E crie o “menu_details_news” :

Como criar um menu no Android

Agora crie o item “Share” no “menu_details_news”:

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

    <item android:id="@+id/btn_share"
        android:icon="@drawable/ic_share"
        app:showAsAction="always"
        android:title="Share" />

</menu>

Precisamos criar também o arquivo com o caminho onde será buscado a image compartilhado no provedor de conteúdo, então crie o xml “file_provider_path.xml”:

Como criar file xml no Android

Edite o caminho dentro do “file_provider_path”:

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

    <external-path name="image-share" path="."/>

</paths>

E finalmente crie o provider no AndroidManifest:

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider_path"/>

        </provider>

Assim ficará todo o código do AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="your.package.name">
  <!-- Esse nome é fictício. Don't change the package's name in your project -->

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".DetailsNewsActivity"
            android:label=""/>
        <activity
            android:name=".SplashActivity"
            android:theme="@style/Splash">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:label="IS IT FAKE NEWS?" />

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider_path"/>

        </provider>

    </application>

</manifest>

Agora só falta implementar o códigodo DetailsNewActivity onde:

  1. Receberemos os dados enviados via onClickListener do item do recyclerview
  2. Recebermos os dados vindos da classe LoadDetailsNews através da interface ILoadDetails com a função “getDetails”
  3. Implementar o botão Home As Up no ActoBar
  4. Criaremos a função “getLocalBitmapUri” para usar o provedor de conteúdo Fileprovider para criar um caminho de “resgate” da image
  5. Criaremos a função “shareNewsImage” pegar a image em formato Bitmap direto do Picasso e para enviar com a ajuda da função “getLocalBitmapUri” a image para outro app
  6. Implementaremos no menu_details_news na nossa Activity e as eventos no botão share para compartilhar a imagem e no botão home do ActionBar para fechar a Activity.
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.view.Menu
import android.view.MenuItem
import androidx.core.content.FileProvider.getUriForFile
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import kotlinx.android.synthetic.main.activity_details_news.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.Exception

class DetailsNewsActivity : AppCompatActivity(), ILoadDetails {

    private var urlImage: String? = null
    private var urlDetails: String? = null
    private var loadDetailsNews: AsyncTask<Void, Void, ArrayList<String>>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_details_news)

        urlImage = intent.getStringExtra("IMAGE")
        urlDetails = intent.getStringExtra("DETAILS")

        txt_title_detail!!.text = intent.getStringExtra("TITLE")
        Picasso.get().load(urlImage).into(image_detail)
        loadDetailsNews = LoadDetailsNews(this, urlDetails!!)
        loadDetailsNews!!.execute()

        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
    }

    fun shareNewsImage(){
        Picasso.get().load(urlImage).into(object : Target{

            override fun onPrepareLoad(placeHolderDrawable: Drawable?) { }

            override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) { }

            override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
                val intent = Intent(Intent.ACTION_SEND)
                intent.setType("image/*")
                intent.putExtra(Intent.EXTRA_STREAM, getLocalBitmapUri(bitmap!!))
                startActivity(Intent.createChooser(intent, "Share"))
            }

            private fun getLocalBitmapUri(bitmap: Bitmap): Uri? {
                var bitmapUri: Uri? = null
                try {

                    val file = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES),
                                "shareimage" + ".png")

                    val out =FileOutputStream(file)
                    bitmap.compress(Bitmap.CompressFormat.PNG, 90, out)
                    out.close()

                    bitmapUri = getUriForFile(applicationContext,
                        applicationContext.packageName + ".fileprovider",
                        file)

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

        })
    }


    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_activity_details_news, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val id:Int = item.itemId

        when(id){
            android.R.id.home -> finish()
            R.id.btn_share -> shareNewsImage()
        }

        return super.onOptionsItemSelected(item)
    }


    override fun getDetails(details: ArrayList<String>) {
        for (index in 0..details.size-1){
            if (index == details.size-1){
                txt_detail.append("\n" + details[index] + "\n")
            }else{
                txt_detail.append(details[index] + "\n\n")
            }
        }
    }
}

Já conseguimos mandar os dados para o “DetailsNewsActivity” através de um item do Recyclerview, carregar o texto com os detalhes da noticia em tempo real e o mais legal: Podemos compartilhar a verdade com apenas o click de um botão!

Nosso app anti-fake news está pronto! Espero que tenha te ajudado tudo que foi apresentado aqui!

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

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

Deixe um comentário