Trabalhando com tarefas em background

Jack Miras
Android Dev BR

--

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 de nós a levam consideração? Neste artigo iremos entender do 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 de mais 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 Thread lançando a exceção NetworkOnMainThreadException.

O Exemplo a cima 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.

Uma das partes mais interessantes dessa classe é que existe uma instância de um Handler, eles são normalmente utilizados para agendar mensagens e blocos de códigos para serem executados no futuro ou para enfileirar ações que serão executadas em uma Thread diferente da sua Thread atual. No entanto no exemplo a cima a ideia do Handler é ter pequenos pedaços de código que são referenciados na instância do switch dentro do método handleMessage(), dessa forma quando você enviar uma mensagem para o objeto do Handler você está dizendo para ele executar aquela determinada parte do AsyncTask.

Mas vamos ao que interessa, vamos ver como um AsyncTask é definido quando precisamos dele na vida real:

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:

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.

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 a 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 nos 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 e 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:

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:

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

No exemplo de código acima nós definimos a nossa 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

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.

Apos 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

Como pode ser observado a cima 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 a cima 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

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 resultReceiver declarado 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 a cima 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

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 intent que 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 MainActivity isso 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

--

--

Jack Miras
Android Dev BR

Experienced in Software & DevOps Engineering. Passionate about technology. Outside of work, I enjoy coffee, music, and spending time with dogs and guinea pigs.