Android

8 dez, 2016

Trabalhando com tarefas em background no Android

Publicidade

Antes de começarmos é importante que você saiba que os método e técnicas demonstrados nesse artigo são a forma mais purista de implementar tarefas em background. Caso você seja um programador mais experiente ou tem uma certa experiência é recomendável que você utilize técnicas, ferramentas ou bibliotecas como por exemplo o RxJava.

Não faça nada que bloqueie a Main Thread… Muitos de nós, programadores, já ouvimos essa frase, porém quantos a levam consideração? Neste artigo iremos entender o por quê devemos levar essa frase a sério e que existem maneiras simples e elegantes de resolver esse problema.

Quando nós falamos sobre Thread, já sabemos (ou deveríamos saber) que uma aplicação Android tem pelo menos uma Main Thread. Por conta de sua importância essa Thread é criada junto com o processo da aplicação, que cria também um instância da classe Application. As principais responsabilidades da Main Thread são renderizar a interface do usuário, lidar com a interação do usuário e inicializar as Activities. Sabendo disso, é de suma importância que você entenda que qualquer código que você adicionar em sua Activity será executado com os recursos que sobraram após o carregamento da Main Thread e dessa forma seu app continuará respondendo ao usuário.

Como você já deve ter percebido, nossa Main Thread não tem tantos recursos disponíveis quanto imaginávamos. Porém em muitos casos parece não ser nada demais adicionar algum código extra na sua Activity, no entanto alguns códigos implementados podem demorar tanto para serem executados que eles simplesmente não serão permitidos pela plataforma Android. Um exemplo disso são casos em que uma chamada HTTP, UDP ou quaisquer chamadas utilizando a rede são realizadas na Main Thread. Esse tipo de chamada normalmente demora tanto para ser executado que o Android simplesmente não permite que as mesmas sejam realizadas na Main Threadlançando a exceção NetworkOnMainThreadException.

O exemplo acima pareceu muito extremo? Então vamos analisar uma outra situação: imagine o cenário em que você tem uma lista na sua Activity e que essa lista exibe um conjunto de produtos que estão armazenados no banco de dados SQLite do seu projeto. Nesse cenário existe uma quantidade considerável de código que precisa ser implementada para que a sua lista exiba o conjunto de produtos… Lembre-se que todo esse código estará rodando na sua Main Thread!

Ainda sim você pode estar pensando… Mas eu carrego só alguns produtos na lista! Porém, imagine o que acontecerá quando seu pequeno conjunto de produtos for de uma ordem 10 ou 100 vezes maior do que é atualmente?

Se sua resposta foi, eu provavelmente refatoraria o código… Pare para pensar quantas vezes durante a sua carreira de programador você teve a oportunidade de voltar em um código já escrito e refatora-lo utilizando as arquiteturas e design patterns da maneira que deveria?

No exemplo acima o problema mais óbvio que ocorrerá na sua aplicação seria que a medida que o número de produtos vai aumentando mais lento carregamento da sua Activity ficará, até que chegará em um ponto em que toda vez em que o usuário à abrir a Activity ela travará e bloqueará as interações do usuário e provavelmente mostraria a mensagem:

O aplicativo x não está respondendo. Você deseja fechá-lo?

Como você já deve saber aplicativos lentos ou que costumam travar não são bem vistos pelo usuário final além de terem uma baixa taxa de conversão e uma alta taxa de desinstalações.

Você pode estar se perguntando… “Ok, então como eu faço para executar em background as tarefas que não são de responsabilidade da Main Thread?”. Felizmente o SDK fornece alguns recursos que nos auxiliam muito na hora de executar nossas tarefas em background, os mais comuns são AsyncTask, Service e IntentService. Porém nesse artigo nós vamos focar no AsyncTask e no IntentService pois os mesmo são os mais simples de implementar quando o assunto é trocar informações entre nossa tarefa em background e nossa Activity.

AsyncTask

Esse é sem duvidas um bom jeito de se executar tarefas em background! AsyncTask’s são muito convenientes pois eles podem ser facilmente criados dentro ou fora da sua Activity e com algumas poucas linhas de código sua tarefa estará rodando em background.

O primeiro de dois métodos muito importantes que você encontrará utilizando esse recurso é doInBackground() que é o método que você deverá sobrescrever para dizer o que você quer processar em background. O segundo é o método onPostExecute() que é executado na Main Thread e é utilizado para a comunicação entre o seu AsyncTask e sua Activity.

Mas o que o AsyncTask está realmente fazendo? Eu recomendo que você vá no Android Studio e veja o código da classe usando ctrl-click no nome da mesma.Porém, eu gostaria de ressaltar um trecho do código desse recurso que diz muito sobre como as Threads são gerenciadas dentro dele.

private static class InternalHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

Agora que já sabemos como o AsyncTask funciona fica a dúvida… E o que acontece quando temos mais de uma tarefa e ambas devem ser executadas assincronamente em uma determinada ordem? Algumas pessoas simplesmente iniciariam o AsyncTask da segunda tarefa no método onPostExecute() da primeira tarefa executada, no entanto esteticamente e tecnicamente esse tipo de código não é legível e manutenível. Veja o exemplo a seguir:

public class BuscaProdutosPorGenero extends AsyncTask<Void, Void, Void> {
    
    @Override
    protected Void doInBackground(Void... params) {
        // Método que processa algo assíncronamente.
        buscaProdutos();
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        // Método que envia os produtos encontrados para a main thread.
         enviarProdutosParaMainThread();

        // Novo AsyncTask para a nova tarefa que precisa ser executada sequêncialmente.
        class BuscaProdutosRelacionados extends AsyncTask<Void, Void, Void> {
            
              @Override
              protected Void doInBackground(Void... params) {
                  // Método que busca uma lista de produtos relacionados aos produtos retornados anteriormente.
                  buscaProdutosRelacionados();
                  return null;
              }
              @Override
              protected void onPostExecute(Void aVoid) {
                  super.onPostExecute(aVoid);
                  // Método que envia os produtos relacionados para a main thread.
                  enviaProdutosrelacionadosParaMainThread();
              }
        }
    }
}

Se você ainda não está convencido de que essa não é uma boa alternativa, imagine como esse código seria, caso você tivesse quatro ou cinco tarefas que precisassem ser executadas em uma dada ordem.

public class BuscaProdutosAninhados extends AsyncTask<Void, Void, Void> {

    @Override
    protected Void doInBackground(Void... params) {
        // Busca por produtos que estejam em promoção.
        executaPasso1();
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);

        class BuscaProdutosPorGenero extends AsyncTask<Void, Void, Void> {

            @Override
            protected Void doInBackground(Void... params) {
                // Busca por outros produtos que sejam do genero dos produtos da promoção.
                executaPasso2();
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);

                class BuscaProdutosRelacionados extends AsyncTask<Void, Void, Void> {

                    @Override
                    protected Void doInBackground(Void... params) {
                        // Busca Por produtos relacionados aos produtos da promoção.
                        executaPasso3();
                        return null;
                    }

                    @Override
                    protected void onPostExecute(Void aVoid) {
                        super.onPostExecute(aVoid);

                        class BuscaProdutosSubstituiveis extends AsyncTask<Void, Void, Void> {

                            @Override
                            protected Void doInBackground(Void... params) {
                                // Busca por produtos que possam subistituir os produtos que estão em promoção.
                                executaPasso4();
                                return null;
                            }

                            @Override
                            protected void onPostExecute(Void aVoid) {
                                super.onPostExecute(aVoid);

                                class BuscaProdutosGenericos extends AsyncTask<Void, Void, Void> {

                                    @Override
                                    protected Void doInBackground(Void... params) {
                                        // Busca por produtos genericos possam subistituir os produtos em promoção.
                                        executaPasso5();
                                        return null;
                                    }
                                }
                                TarefaAssincrona5 tarefaAssincrona5 = new TarefaAssincrona5();
                                tarefaAssincrona5.execute();
                            }
                        }
                        TarefaAssincrona4 tarefaAssincrona4 = new TarefaAssincrona4();
                        tarefaAssincrona4.execute();
                    }
                }
                TarefaAssincrona3 tarefaAssincrona3 = new TarefaAssincrona3();
                tarefaAssincrona3.execute();
            }
        }
        TarefaAssincrona2 tarefaAssincrona2 = new TarefaAssincrona2();
        tarefaAssincrona2.execute();
    }
}

Conseguiu visualizar como esse código fica ilegível? E se eventualmente nós precisássemos mudar a ordem de alguma dessas cinco tarefas?

Além da ilegibilidade e da dificuldade para alterar a ordem de execução das nossas tarefas, a situação acima tem um problema conhecido como callback hell, esse é um problema que ocorre quando novos passos de execução são iniciados dentro do callback de um processo que está terminando. No nosso contexto esse problema acontece quanto iniciamos um novo AsyncTask no método onPostExecute() de um AsyncTask que está sendo finalizado.

É uma situação complicada não é mesmo? Mas não se preocupe, pois nosso SDK fornece uma outra alternativa que nos permite executar várias tarefas assíncronas de forma mais organizada, sem termos que nos preocupar com a gerencia de Threads, e essa alternativa chama se IntentService.

IntentService

Como o próprio nome sugere a classe IntentService é baseada na classe Service. Dessa forma, é natural que existam várias semelhanças entre as mesmas. Tendo isso em mente é importante que você saiba algumas coisas antes de prosseguirmos para o próximo código de exemplo.

Para que o IntentService seja executado, o programador iniciará suas tarefas realizando uma chamada para o método startService(Intent) dentro da Activity ou Fragment que precisa executar algo assíncrono, o objeto Intent passado como parâmetro no método anteriormente chamado pode ser utilizado para enviar informações para o seu IntentService. Tendo isso em mente é importante que você saiba que cada tarefa será inicializada e processada usando um Worker Thread, além disso o IntentService encerra a execução, fecha o Worker Thread, automaticamente quando todas as tarefas são de alguma forma executadas.

Outro ponto importante que vale ressaltar é que a comunicação de retorno, ou seja, o envio de mensagem do IntentService para a Activity que está rodando na Main Thread é feito via ResultReceiver.

Visto que já sabemos os conceitos básicos do funcionamento do IntentService a primeira coisa que precisamos fazer para começar a implementar o processamento assíncrono e ordenado das nossas tarefas é criar nosso IntentService.

Criando IntentService

Para criar um IntentService você precisa implementar seu códigos nos moldes do código a seguir:

public class TarefaAssincronaIntentService extends IntentService {
    
   public TarefaAssincronaIntentService() {
        super("name");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
      // Dentro desse método será implementada a execução da tárefa.
    }
}

No entanto somente criar a nosso IntentService não é o suficiente para que ele funcione corretamente pois além de criar a nossa classe nós precisamos declará-la no AndroidManifest da mesma forma que fazemos quando estamos implementando um Service. Veja o código a seguir:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.background_tasks">
    <application
        ....
        <service
            android:name=".TarefaAssincronaIntentService"
            android:exported="false"/>
    </application>
</manifest>

Agora que já criamos nosso IntentService e o declaramos no AndroidManifest podemos começar a implementar a estrutura de execução das nossas tarefas assíncronas. O passo seguinte é implementar uma estrutura totalmente desacoplada do nosso IntentService para que possamos implementar o processamento das nossas tarefas, dessa forma o IntentService fica responsável apenas por iniciar o processamento das tarefas e retornar os resultados para a nossa Activity.

Estrutura isolada para a execução das tarefas

public class ModuloDaTarefa {

    // ArrayList que armazenará os resultados das buscas das tarefas assíncronas.
    private ArrayList<Produto> produtosResult;

    public void start(IntentServiceCallback callback) {
        // Callback para a comunicação com o IntentService.
    }

    public Message obtemMessage(int msgWhat) {
    }

    private class IntentServiceHandler extends Handler {

        private final IntentServiceCallback callback;

        public IntentServiceHandler(IntentServiceCallback callback) {
            // Callback para a comunicação com o IntentService.
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constantes.BUSCAR_PRODUTOS:
                    // Código que busca os produtos aqui.
                    break;
                case Constantes.BUSCAR_PRODUTOS_POR_GENERO:
                    // Código que busca os produtos por gênero aqui.
                    break;
                case Constantes.BUSCAR_PRODUTOS_RELACIONADOS:
                    // Código que busca os produtos relacionados aqui.
                    break;
                case Constantes.BUSCAR_PRODUTOS_SUBSTITUIVEIS:
                    // Código que busca os produtos substituiveis aqui.
                    break;
                case Constantes.BUSCAR_PRODUTOS_GENERICOS:
                    // Código que busca os produtos genéricos aqui.
                    break;
                case Constantes.SUCESSO:
                    // Callback que informa se o processamento ocorreu com sucesso.
                    break;
                case Constantes.FALHA:
                    // Callback que informa se o processamento falhou.
                    break;
            }
        }
    }
}

No exemplo de código acima nós definimos a estrutura para a execução das nossas tarefas assíncronas. Observando o código do exemplo é possível ver que foi definido um ArrayList de produtos que armazenará os resultado das buscas que serão executadas por cada uma das nossas tarefas, também temos um construtor que recebe como parâmetro a interface(callback) que será utilizada para a comunicação com o IntentService e por fim nós temos a definição de um Handler contendo um switch que será utilizado para gerenciar a execução de cada tarefa na ordem necessária. Porém, antes de prosseguirmos com a implementação das tarefas assíncronas, vamos ver como foi definida e qual o propósito da nossa interface de comunicação com o IntentService.

Interface para comunicação

public interface IntentServiceCallback {
    void onSucesso(ArrayList<Produto> produtos);
    void onFalha();
}

A interface de comunicação é uma interface bastante simples que será utilizada como callback, nela definimos dois métodos, onSucesso() que é o método que utilizaremos para informar e enviar os resultados da execução das nossas tarefas quando elas forem executadas com sucesso. Já o método onFalha() será utilizado caso o processamento das nossas tarefas falhe e nós precisemos informar a nossa Activity que o processamento falhou.

Após implementar a nossa interface de comunicação entre nossa estrutura desacoplada e nosso IntentService o próximo passo é implementar de fato a execução das nossas tarefas.

Implementação das tarefas a serem executadas

public class ModuloDaTarefa {

    private ArrayList<Produto> produtosResult;

    public void start(IntentServiceCallback callback) {
        IntentServiceHandler handler = new IntentServiceHandler(callback);
        handler.sendMessage(obtemMessage(Constantes.BUSCAR_PRODUTOS));
    }

    public Message obtemMessage(int msgWhat) {
        Message message = new Message();
        message.what = msgWhat;
        return message;
    }

    private class IntentServiceHandler extends Handler {

        private final IntentServiceCallback callback;

        public IntentServiceHandler(IntentServiceCallback callback) {
            this.callback = callback;
        }

        @Override
        public void handleMessage(Message msg) {
            List<Produto> produtos;
            switch (msg.what) {
                case Constantes.BUSCAR_PRODUTOS:
                    try {
                        produtos = buscarProdutos();
                        produtosResult.addAll(produtos);
                        sendMessage(obtemMessage(Constantes.BUSCAR_PRODUTOS_POR_GENERO));
                    } catch (Exception e) {
                        sendMessage(obtemMessage(Constantes.FALHA));
                    }
                    break;
                case Constantes.BUSCAR_PRODUTOS_POR_GENERO:
                    try {
                        produtos = buscarProdutosPorGenero();
                        produtosResult.addAll(produtos);
                        sendMessage(obtemMessage(Constantes.BUSCAR_PRODUTOS_RELACIONADOS));
                    } catch (Exception e) {
                        sendMessage(obtemMessage(Constantes.FALHA));
                    }
                    break;
                case Constantes.BUSCAR_PRODUTOS_RELACIONADOS:
                    try {
                        produtos = buscarProdutosRelacionados();
                        produtosResult.addAll(produtos);
                        sendMessage(obtemMessage(Constantes.BUSCAR_PRODUTOS_SUBSTITUIVEIS));
                    } catch (Exception e) {
                        sendMessage(obtemMessage(Constantes.FALHA));
                    }
                    break;
                case Constantes.BUSCAR_PRODUTOS_SUBSTITUIVEIS:
                    try {
                        produtos = buscarProdutosSubstituiveis();
                        produtosResult.addAll(produtos);
                        sendMessage(obtemMessage(Constantes.BUSCAR_PRODUTOS_GENERICOS));
                    } catch (Exception e) {
                        sendMessage(obtemMessage(Constantes.FALHA));
                    }
                    break;
                case Constantes.BUSCAR_PRODUTOS_GENERICOS:
                    try {
                        produtos = buscarProdutosGenericos();
                        produtosResult.addAll(produtos);
                        sendMessage(obtemMessage(Constantes.SUCESSO));
                    } catch (Exception e) {
                        sendMessage(obtemMessage(Constantes.FALHA));
                    }
                    break;
                case Constantes.SUCESSO:
                    callback.onSuccess(produtosResult);
                    break;
                case Constantes.FALHA:
                    callback.onFailure();
                    break;
            }
        }
    }
}

Como pode ser observado acima, cada uma de nossas tarefas tem um escopo de execução bem definido no switch, além disso cada uma delas aponta automaticamente para a próxima de forma muito mais simples do que no exemplo em que utilizamos AsyncTasks.

Outro fator importante é que a medida em que nossas tarefas são processadas nós podemos definir de forma muito fácil como ela vai reagir caso o processamento aconteça com sucesso ou falhe, no exemplo acima foi utilizado um bloco try catch para direcionar a execução para o callback de sucesso ou falha, já no exemplo utilizando AsyncTask o nosso único canal de comunicação com a Activity é o onPostExecute o que aumenta um pouco a complexidade quando é necessário informar se o processamento ocorreu com sucesso ou não.

Agora que já implementamos a execução das nossas tarefas assíncronas é hora de implementarmos nosso IntentService e sua comunicação com a nossa activity.

Implementando IntentService

public class TarefaAssincronaIntentService extends IntentService
        implements IntentServiceCallback {

    private ResultReceiver resultReceiver;

    public TarefaAssincronaIntentService() {
        super("name");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent.hasExtra(Constantes.RESULT_RECEIVER_OBJECT)) {
            resultReceiver = intent.getParcelableExtra(Constantes.RESULT_RECEIVER_OBJECT);
            if (resultReceiver != null) {
                ModuloDaTarefa moduloDaTarefa = new ModuloDaTarefa();
                moduloDaTarefa.start(this);
            }
        }
    }

    @Override
    public void onSuccesso(ArrayList<Produto> produtos) {
        Bundle bundle = new Bundle();
        bundle.putSerializable(Constantes.PRODUTOS_OBJECT, produtos);
        resultReceiver.send(Constantes.SUCESSO_REQUEST_CODE, bundle);
    }

    @Override
    public void onFalha() {
        resultReceiver.send(Constantes.FALHA_REQUEST_CODE, null);
    }
}

Como pode ser observado nosso código contém a declaração do campo resultReceiver tendo como tipo a classe ResultReciver que neste contexto é utilizado para a comunicação do nosso TarefaAssincronaIntentService com a nossa Activity aqui nós temos também a sobrescrita do método onHandleIntent() proveniente da extensão da classe IntentService.

No método onHandleIntent() é onde nós devemos implementar o código que gostaríamos de executar em background, para isso, estamos checando se um objeto ResultReceiver foi nos enviado via Intent e caso tenhamos algum objeto que no nosso Intent tenha como chave a constante RESULT_RECEIVER_OBJECT nós o adicionamos para o campo resultReceiverdeclarado anteriormente e então nós executamos uma nova checagem para que caso ele não seja nulo nós inicializemos a execução do código na nossa classe ModuloTarefa, onde implementamos a lógica que precisa ser executada em background.

Além disso o código acima implementa a interface de callback anteriormente demonstrada, com isso nosso código passa a sobrescrever os métodos onSucesso() e onFalha() para que possamos receber os resultados vindos da nossa classe ModuloTarefa.

Em ambos os métodos nós utilizamos o campo resultReceiver chamando o método send() para enviar o resultado recebido da classe ModuloTarefa para a nossa Activity porem é importante ressaltar que o método send aceita como parâmetros apenas um int como resultCode e um Bundle como resultData e por esse motivo no método onSucesso() foi construído um objeto bundle que utilizamos para armazenar nossa lista de produtos resultante da classe ModuloTarefa.

Implementando Activity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        TarefasAssincronasResultReceiver resultReceiver = new TarefasAssincronasResultReceiver(new Handler());
        Intent intent = new Intent(this, TarefaAssincronaIntentService.class);
        intent.putExtra(Constantes.RESULT_RECEIVER_OBJECT, resultReceiver);
        
        startService(intent);
    }


    private class TarefasAssincronasResultReceiver extends ResultReceiver {

        public TarefasAssincronasResultReceiver(Handler handler) {
            super(handler);
        }

        private Parcelable.Creator<String> CREATOR;

        @Override
        protected void onReceiveResult(int resultCode, Bundle resultData) {
            // Precessar resultados retornados do IntentService.
        }
    }
}

Por fim chegamos a nossa MainActivity, nela nós temos a sobrescrita do método onCreate() que é utilizado para iniciar nosso TarefaAssincronaIntentService. A implementação do método onCreate()consiste na declaração dos campos tarefasAssincronasResultReceiver e intentque tem como tipos as classes ResultReceiver e Intent, a declaração dos campos é seguida da adição do campo tarefasAssincronasResultReceiver ao campo intent para que o mesmo seja enviado via Intent para o nosso IntentService, por fim utilizamos o campo intent que contem o nosso campo tarefasAssincronasResultReceiver para iniciar nosso IntentService chamando startService(intent).

Caso você tenha lido o código acima e tenha ficado em dúvida sobre qual a real função da classe interna TarefasAssincronasResultReceiver não se preocupe isso é perfeitamente compreensível, a classe TarefasAssincronasResultReceiver foi implementada como uma classe interna da nossa MainActivity pois como dito anteriormente o IntentService se comunica com a nossa Activity usando a classe ResultReceiver como callback, e como você pode ver nossa classe interna extende ResultReceiver o que faz da nossa classe interna uma representação valida para o callback que é requerido pelo IntentService como forma de comunicação com a nossa interface de usuário.

Conseguiu perceber a conexão? Nossa classe TarefasAssincronasResultReceiver utilizada para declarar o campo tarefasAssincronasResultReceiver que foi enviado via Intent para o nosso IntentService é representada nele pelo objeto resultReceiver se lembra? Se você conseguiu identificar essa conexão você pode ser pensando OK, OK, mas qual o significado do código existem no nosso TarefasAssincronasResulReceiver bom… Nessa classe interna nós temos um construtor um campo do tipo Parceable chamado CREATOR e um método sobrescrito chamado onReceiveResult(), todo esse código é padrão e tem sua implementação exigida quando estendemos a classe ResultReceiver.

Apesar da classe TarefasAssincronasResultreceiver ser muito simples por ter apenas um construtor, campo e método seu método tem um papel muito importante. Esse método é o que será acionado como callback quando o campo resultReceiver for acionando na classe TarefaAssincronaIntentService e por seu contexto estar atrelado a uma classe interna na nossa MainActivityisso nos permite usar esse método callback acionando assim alguma ação na nossa MainActivity ou atualizar alguma das views implementadas.

Considerações finais

Documentação oficial

Contatos

Viu algo de errado? Tem uma sugestão? Você pode me encontrar pelo e-mail jackmiras@gmail.com e twitterFeedbacks sempre são bem-vindos!

Comunidade Android

Participe do maior forum sobre Android no Brasil no slack Android Dev BR.
Site: http://www.androiddevbr.org | Convite: http://slack.androiddevbr.org