Múltiplas Threads usando AsyncTask no Android

Neste post vamos aprender a gerenciar threads de execução em um aplicativo Android, usando a classe AsyncTask. Ao final, seu app será capaz de realizar várias tarefas simultaneamente e oferecer uma melhor experiência aos usuários.

Processos e threads

Uma característica muito importante dos processadores modernos é a capacidade de executar instruções tão rapidamente que os programas que utilizamos em um computador ou smartphone parecem estar sendo executados simultaneamente. Essa capacidade é chamada de multiprocessamento. Além disso, cada programa ou processo pode ter diversas linhas de execução, denominadas threads. A diferença entre um processo e uma thread está na forma como seus recursos são compartilhados. Cada processo tem uma área de memória apenas sua. Isso impede que dois processos compartilhem diretamente dados como variáveis e informações de estado. Uma thread é uma linha de execução pertecente a um processo. Assim, múltiplas threads de um mesmo processo podem compartilhar diretamente seus recursos.

Múltiplas threads no Android

Você já deve ter percebido que um processo, no Android, refere-se a um app. Ótimo! Por padrão, cada aplicativo Android é executado em um processo e inicia com apenas uma thread, a thread UI, referente à interface gráfica. Que tal melhorarmos essa perspectiva? Imagine por 10 segundos que tipo de problema pode surgir se seu app possui apenas uma thread de execução.

Exatamente! Imagine que um app precisa fazer download de uma imagem e essa imagem possui 5MB. Sua conexão é muito ruim, só consegue baixar a 100 kB/s. Demoraria 50 segundos para baixar a imagem. Se seu app tiver apenas uma thread, o processo de download será executado na thread UI e, consequentemente, vai travar a interface gráfica por pelo menos 50 segundos. Você gosta de um app travado? Não. Essa é a primeira motivação para uso da múltiplas threads e da classe AsyncTask.

Classe AsyncTask

A classe AsyncTask permite que você inicie uma nova thread, realize uma tarefa na nova thread e retorne informações do processamento para a thread principal. Essas informações podem ser usadas para atualizar a interface gráfica, mostrar progressos e resultados. De acordo com a referência no Android Developers, AsyncTask é uma classe que encapsula as classes Thread e Handler de maneira a facilitar sua utilização. Além disso, essa classe foi desenvolvida para realização de tarefas com duração na ordem de segundos. Para tarefas com durações muitos maiores, é recomendado o uso de outras classes e interfaces como Executor, ThreadPoolExecutor e FutureTask.

AsyncTask significa “tarefa assíncrona” e é definida no Android Developers como uma tarefa que é executa em uma thread no plano de fundo e cujos resultados são exibidos na interface gráfica. Para usá-la, deve-se criar uma subclasse de AsyncTask e instanciar os quatro principais métodos da classe: onPreExecute(), doInBackground(), onProgressUpdate() e onPostExecute(). Veja abaixo o esqueleto de uma subclasse de AsyncTask. Observe os três parâmetros de definição de uma AsyncTask: Params, Progress e Result, além da definição dos métodos exigidos pela superclasse.

import android.os.AsyncTask;

public class LongTask extends AsyncTask<Params, Progress, Result>{

    @Override
    protected void onPreExecute() {
        // Executa na thread principal uma vez antes do início da nova thread
    }

    @Override
    protected Result doInBackground(Params... params) {
        // Define a operação que será executada na nova thread
        return null;
    }

    @Override
    protected void onProgressUpdate(Progress... progress) {
        /* Executa na thread principal quando o método 
           publishProgress() é chamado */
    }

    @Override
    protected void onPostExecute(Result result) {
        // Executa na thread principal após o término da nova thread
    }
}

Params

É a definição de qual classe de objeto será a entrada da sua tarefa assíncrona. Imagine que você quer processar uma String no plano de fundo, então Params será substituído por String nesse template. A princípio, Params pode ser qualquer classe de objeto que você queira. Veja que sua escolha de Params influencia no parâmetro de entrada do método doInBackground(). Os dois precisam ser iguais.

Progress

É a definição de qual classe de objeto servirá para indicar o progresso da tarefa. Novamente, qualquer objeto pode ser utilizado, mas geralmente utiliza-se Integer, Long ou Double. Note que isso influencia o parâmetro de entrada do método onProgressUpdate().

Result

Define qual classe de objeto é retornado após a execução da tarefa. Qualquer classe pode ser utilizada, desde que você associe corretamente o mesmo tipo à saída do método doInBackground() e a entrada de onPostExecute(). Eles precisam ser iguais.

onPreExecute()

Esse método é executado antes da criação da nova thread e da realização da tarefa definida. É utilizada para ajustar os valores iniciais dos recursos que serão processados na tarefa. Isso inclui variáveis, objetos referentes a elementos da interface gráfica, entre outros. Note que onPreExecute() é realizado na thread principal, aquela que gerencia a interface gráfica. Portanto não tente colocar tarefas longas aqui ainda, a não ser que você sinta prazer ao ver na tela que o aplicativo parou de funcionar.

doInBackground()

Enfim, esse método será executado na nova thread, no plano de fundo. Insira aqui o código da sua tarefa e seja feliz. Perceba que o parâmetro de entrada aqui é o que você definiu como Params. Não importa o que tiver sido definido como Params, o método doInBackground() terá acesso ao vetor de valores de entrada como um vetor e você pode acessá-lo livremente. Além disso ao final, esse método deve retornar um objeto da classe que foi definida como Result. Não esqueça.

onProgressUpdate()

Sua principal função é atualizar a interface gráfica com as informações definidas pelo parâmetro Progress. Durante a execução de doInBackground(), você pode realizar uma chamada ao método publishProgress(), usando como entrada um objeto do tipo que foi definido como parâmetro Progress. Isso invocará onProgressUpdate(). Aproveite para modificar a interface gráfica como achar necessário, atualizar alguns TextView, barras de progresso e o que mais preferir. Já que esse é executado também na thread principal, seja breve, nada de minerar bitcoins aqui.

onPostExecute()

Esse método executa na thread principal, então só deve realizar operações curtas. Ao final da execução da tarefa, temos aqui um método para ajustar a interface gráfica e exibir os resultados do processamento. Esse método tem como entrada um objeto da classe definida como Result. Perceba que o valor de saída de doInBackground() será usado como entrada de onPostExecute().

Como usar AsyncTask em um aplicativo

Vamos desenvolver um app usando AsyncTask! Neste app de demonstração, Super Threads, teremos apenas uma tela. Bem simples. Terá alguns elementos de texto para mostrarmos que operação o app está realizando, uma barra de progresso e dois botões: um para iniciar uma tarefa com AsyncTask e outro para cancelar a tarefa em andamento. A tarefa que vamos realizar é apenas demonstrativa: contar de 0 até 1000 com intervalos de 10 milissegundos. Isso serve para simular uma operação longa em andamento. Você poderá substituir o código contador com qualquer operação que desejar. Vamos aos códigos!

Arquivo styles.xml

Algumas alterações no arquivo styles.xml. Não são necessárias para o funcionamento do app, apenas servem para melhorar ao visual. Pura vaidade.

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">#0E95F9</item>
        <item name="colorPrimaryDark">#0087CE</item>
        <item name="colorAccent">#F9720E</item>
    </style>

</resources>

Arquivo activity_main.xml

Esse é o código XML que define a interface gráfica da Activity principal. Nela temos dois elementos TextView, uma ProgressBar e dois elementos Button. Usaremos os TextViews para exibir mensagens indicando a operação atual e instruções para o usuário sobre o que ele pode fazer. A ProgressBar será atualizada conforme o contador é atualizado na thread secundária, que ainda vamos definir. Os botões “Iniciar” e “Cancelar” controlam o fluxo da operação que definiremos na subclasse de AsyncTask.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="#0E95F9"
    android:alpha="0.8"
    tools:context="com.dragaosemchama.superthreads.MainActivity">

    <TextView
        android:id="@+id/textInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="65dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:text="Clique em Iniciar para começar uma tarefa demorada"
        android:textAlignment="center"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textColor="@android:color/background_light"
        android:textSize="25dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:layout_constraintLeft_creator="1"
        tools:layout_constraintTop_creator="1" />

    <TextView
        android:id="@+id/textMoreInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:text="A tarefa será realizada em outra thread"
        android:textAlignment="center"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textColor="@android:color/background_light"
        android:textSize="15dp"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintRight_creator="1"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="11dp"
        app:layout_constraintTop_toBottomOf="@+id/textInfo"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="parent" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:paddingLeft="40dp"
        android:paddingRight="40dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:layout_constraintBottom_creator="1"
        tools:layout_constraintLeft_creator="1"
        tools:layout_constraintRight_creator="1"
        tools:layout_constraintTop_creator="1" />

    <Button
        android:id="@+id/buttonStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Iniciar"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintBottom_creator="1"
        android:layout_marginStart="72dp"
        app:layout_constraintBottom_toBottomOf="@+id/buttonCancel"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="@+id/buttonCancel"
        android:layout_marginLeft="72dp"
        android:onClick="buttonStartCallback"/>

    <Button
        android:id="@+id/buttonCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Cancelar"
        tools:layout_constraintRight_creator="1"
        tools:layout_constraintBottom_creator="1"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginEnd="76dp"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginBottom="46dp"
        android:layout_marginRight="76dp"
        android:onClick="buttonCancelCallback"/>

</android.support.constraint.ConstraintLayout>
Veja que bonito!

Arquivo MainActivity.java

O código da Activity principal. Nele temos a definição um objeto para cada elemento da interface gráfica e a definição de um objeto LongTask. Essa classe é a subclasse de AsyncTask que vamos criar logo logo. No método onCreate(), fazemos o link entre cada objeto e seu respectivo elemento definido no XML. Também aumentamos um pouquinho o tamanho da barra de progresso.

Observe a definição das duas funções que executam quando os botões são clicados. Elas precisam ter o mesmo nome definido no parâmetro onClick no código XML do elemento em questão.

O método startButtonCallback() é onde instanciamos o objeto longTask e declaramos a quais elementos da interface gráfica a thread secundária poderá acessar e modificar. Note que a definição dos métodos setProgressBar(), setTextInfo() e setTextMoreInfo() é feita pelo desenvolvedor e precisa estar no código da subclasse de AsyncTask. Ao fim, chamamos o método longTask.execute() e damos início à thread secundária. A última função, cancelButtonCallback() envia um sinal de cancelamento às operações da thread. Também vamos implementar essa funcionalidade no código da classe LongTask.

package com.dragaosemchama.superthreads;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    TextView textInfo;
    TextView textMoreInfo;
    ProgressBar progressBar;
    Button buttonStart;
    Button buttonCancel;

    LongTask longTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try { getSupportActionBar().hide(); }
        catch (NullPointerException E) {E.printStackTrace();}

        textInfo = (TextView) findViewById(R.id.textInfo);
        textMoreInfo = (TextView) findViewById(R.id.textMoreInfo);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);
        buttonStart = (Button) findViewById(R.id.buttonStart);
        buttonCancel = (Button) findViewById(R.id.buttonCancel);

        progressBar.setScaleY(3f);
    }

    public void buttonStartCallback(View v) {
        longTask = new LongTask();
        longTask.setProgressBar(progressBar);
        longTask.setTextInfo(textInfo);
        longTask.setTextMoreInfo(textMoreInfo);
        longTask.execute();
    }

    public void buttonCancelCallback(View v) {
        if(longTask != null) longTask.cancel(true);
    }
}

Arquivo LongTask.java

A definição da nossa subclasse de AsyncTask requer que saibamos quais são os parâmetros de entrada, progresso e saída (Params, Progress e Result). Considerando que nossa tarefa é uma contagem de 0 a 1000, pré-definida, temos que Params pode ser Void, ou seja, não há um parâmetro de entrada. Para atualizar a barra de progresso de forma adequado, vamos usar um inteiro de 0 a 100, então Progress será do tipo Integer. Não temos um valor para retornar na contagem, então declaramos Result como Void também. Essas definições encontram-se na linha 7.

Acessando os elementos da interface gráfica

A thread que estamos desenvolvendo deverá acessar e modificar os TextViews e a ProgressBar que criamos na MainActivity, portanto, declaramos novamente um objeto para cada elemento. Mas dessa vez, não pegaremos uma referência para cada elemento diretamente das definições no XML, mas a partir dos métodos setTextInfo(), setMoreTextInfo() e setProgressBar(). Lembra como fizemos no código da MainAcitivity? Estávamos apenas instanciando os objetos dentro da classe LongTask.

Definindo o fluxo de operação da AsyncTask

Agora, veremos os métodos sobrescritos da superclasse AsyncTask. Em onPreExecute(), simplemente atualizamos o valor dos TextViews para informar que a operação está sendo realizada e colocamos o indicador da barra de progresso em 0.

O método mais importante é doInBackground(). Ele será realizado na nova thread. Observe o código que inserimos nesse método. Ele é apenas uma contagem de 0 até 1000. A cada passo, pausamos a atividade por 10 milissegundos, assim garantimos que doInBackground() vai demorar não menos que 10 segundos para finalizar. Veja também que após cada pausa, chamamos o método publishProgress() com o valor do progresso como parâmetro de entrada. Além disso, verificamos se a thread recebeu algum sinal de cancelamento. Caso positivo, finalizamos a contagem. Essa é a implementação da funcionalidade de cancelamento.

A chamada de publishProgress() invoca o método onProgressUpdate(). Veja que nesse método, apenas atualizamos o valor da ProgressBar.

O método onCancelled(), que ainda não tinha sido explicado, é invocado quando o método cancel() da AsyncTask é chamado. Nesse método, apenas atualizamos os TextViews para informar ao usuário que a tarefa foi cancelada.

Enfim, onPostExecute() atualiza os TextViews para informar que a tarefa foi concluída.

package com.dragaosemchama.superthreads;

import android.os.AsyncTask;
import android.widget.ProgressBar;
import android.widget.TextView;

public class LongTask extends AsyncTask<Void, Integer, Void>{

    private TextView textInfo;
    private TextView textMoreInfo;
    private ProgressBar progressBar;

    public void setTextInfo(TextView textInfo) {
        this.textInfo = textInfo;
    }

    public void setTextMoreInfo(TextView textMoreInfo) {
        this.textMoreInfo = textMoreInfo;
    }

    public void setProgressBar(ProgressBar progressBar) {
        this.progressBar = progressBar;
    }

    @Override
    protected void onPreExecute() {
        if(textInfo != null)
            textInfo.setText("Realizando tarefa demorada...");
        if(textMoreInfo != null)
            textMoreInfo.setText("Clique em Cancelar para terminar a execução");
        if(this.progressBar != null) this.progressBar.setProgress(0);
    }

    @Override
    protected Void doInBackground(Void... params) {

        for(int i=0; i < 1000; i++) {
            try { Thread.sleep(10); }
            catch (InterruptedException e) { e.printStackTrace(); }
            this.publishProgress(i/10);
            if(this.isCancelled()) break;
        }

        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        if(this.progressBar != null) this.progressBar.setProgress(progress[0]);
    }

    @Override
    protected void onCancelled() {
        if(textInfo != null)
            textInfo.setText("Tarefa cancelada :(");
        if(textMoreInfo != null)
            textMoreInfo.setText("Clique em iniciar para tentar novamente");
    }

    @Override
    protected void onPostExecute(Void result) {
        if(textInfo != null)
            textInfo.setText("Tarefa realizada com sucesso :)");
        if(textMoreInfo != null)
            textMoreInfo.setText("Clique em iniciar para fazer novamente");
    }
}

Considerações finais

O exemplo que usamos aqui é bem simples. Você pode vir a perguntar como utilizar os parâmetros Params e Result. Vejamos rapidamente como você faria para alterar Params e torná-lo um objeto String. Trocaríamos a declaração da subclasse de AsyncTask, LongTask para:

public class LongTask extends AsyncTask<String, Integer, Void>

A declaração do método doInBackground() seria alterada para:

protected Void doInBackground(String... params)

Para acessar o primeiro valor passado como parâmetro a partir do método doInBackground(), faríamos:

String value = params[0];

A depender da quantidade de parâmetros passados, você pode acessar os outros índices do vetor params. Enfim, na MainActivity, para iniciar a AsyncTask, você faria:

String input = "the king"
longTask.execute(input);

A partir do que foi dito, aposto que você consegue sozinho descobrir como alterar a classe de Result, se for necessário. Ótimo.

Depois de implementar esse código e aprender a usar AsyncTask é fortemente recomendado que você saia para tomar sorvete.  O código desse projeto está disponível no Github do Dragão! Até a próxima :)

 

David Borges

Um dia... Boom! Dragão sem Chama.

11 thoughts to “Múltiplas Threads usando AsyncTask no Android”

  1. Muito obrigado pelo artigo, eu estava “apanhando” tentando entender este conceito, mesmo consultando o Android Developer, e agora finalmente consegui compreender bem. Não conhecia o site, porém agora, já está nos favoritos. Parabéns pelo excelentíssimo trabalho.

  2. Tenho a AsyncTask criada em uma classe separada, queria colocar uma progressBar a medida que a busca de dados vai avançando, mas não consigo modificar a tela pela AsyncTask, como proceder? Vou ter que colocar a AsyntTask dentro da classe que a chama?

    1. Oi, Everton

      No post estamos usando essa mesma ideia, com a AsyncTask definida em outro arquivo. Para que a AsyncTask possa modificar os elementos da interface gráfica, basta passar os objetos que representam os elementos da interface para a AsyncTask através de métodos auxiliares. Observe as linhas 38 a 44 do arquivo MainActivity e as linhas 13 a 23 do arquivo LongTask.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *