ANR Android: Evitando o Application Not Responding com tarefas paralelas

Este artigo apresenta como as tarefas paralelas devem ser executadas em uma aplicação Android, a fim de evitar os erros conhecidos como ANR (Application Not Responding).

Fique por dentro

Saber trabalhar com tarefas em paralelo na plataforma Android é essencial. Este artigo será útil no entendimento em relação à forma como tarefas paralelas devem ser executadas em uma aplicação Android a fim de evitar os erros conhecidos como ANR (Application Not Responding). Para exemplificar, será desenvolvida uma aplicação de consumo de feeds, a qual fará o download das mesmas a partir de um web service.

As aplicações para smartphones geralmente necessitam buscar recursos de fontes externas para realizar determinadas tarefas ou dar acesso ao usuário a algum conteúdo pertinente do escopo do aplicativo. Por se tratar de programação sequencial conforme mostra a Figura 1, quando a tarefa a ser realizada é custosa em relação ao tempo a ser gasto para realizá-la e o processamento desses recursos ocorre na UIThread conforme mostra a Figura 2, a aplicação é travada até que a instrução seja concluída. Por causa desse travamento, o sistema operacional fecha a aplicação evidenciando uma ANR (Application Not Responding), pois por restrição do próprio sistema operacional, nesse caso Android, o aplicativo não deve ficar mais do que cinco segundos indisponível às interações do usuário.

Para que tal erro não ocorra, é necessário utilizar formas de processamento em paralelo como Threads e tomar o cuidado de evitar a comunicação direta entre qualquer Thread que não seja a principal com a interface do usuário (UI).

Além de evitar o travamento pela indisponibilidade da aplicação em responder às interações do usuário, a programação em paralelo, de forma geral, também pode melhorar o desempenho da aplicação através da divisão a tarefa em partes menores a fim de tornar mais rápida a solução do problema.

Como ponto negativo, programar em paralelo é muitas vezes mais complicado além de trazer para o escopo novos problemas que não ocorrem na programação sequencial como a possibilidade de um método ser executado sem que ainda possua os valores corretos, os quais serão calculados em outro método.

Figura 1. Programação sequencial
Figura 2. Tarefas na UIThread

Contudo, com o objetivo de fornecer ao usuário uma melhor experiência de uso, as instruções mais complexas e/ou demoradas da aplicação como, por exemplo, as requisições para serviços web, o acesso a um banco de dados ou arquivo, devem ser realizadas de forma concorrente para que a aplicação não fique indisponível.

Utilizando Thread

Thread é a forma de um processo se dividir sozinho em duas ou mais tarefas que podem ser executadas ao mesmo tempo de forma concorrente com seu estado variando entre: unstarted, running, suspended e stopped.

A vantagem de se utilizar Thread na aplicação é poder executar mais de uma tarefa ao mesmo tempo, evitando que a aplicação trave à espera de um processo demorado. No caso do sistema Android, essa demora, como já citado anteriormente, irá causar a finalização automática da aplicação enviando um erro ANR. Para executar tanto uma Thread quanto um Runnable, é necessário instanciar um objeto. No caso da classe Thread, para iniciar a execução, basta chamar o método start(), enquanto para a interface Runnable, o método run().

O sistema Android oferece o suporte para a utilização da classe Thread em Java para a realização de tarefas assíncronas, assim como do pacote java.util.concurrent para realização de tarefas em background. Contudo, o desenvolvedor deve ter certo cuidado ao utilizar uma classe que herde diretamente da classe Thread ou implemente a interface Runnable. Isso porque as mesmas não oferecem um recurso padrão para tratar as mudanças de configuração da aplicação como, por exemplo, a mudança do modo de orientação de retrato para paisagem ou vice-versa. A utilização da classe Thread também não oferece um recurso padrão para sincronização com a main thread (UIThread) em casos nos quais é necessário retornar um resultado. Além de todas essas implicações, a inexistência de um processo padrão para cancelamento da classe Thread faz com que a aplicação mantenha a classe executando até que esta termine mesmo que já não seja necessário que todo o processo nela seja executado.

Utilizando Handler

Para solucionar as limitações e suprir as desvantagens geradas para utilização da classe Thread em Java no próprio sistema, o Google desenvolveu a classe Handler, a qual está disponível desde a API level 1. Ela permite enviar e processar objetos da classe Message e da interface Runnable que estão associados a uma fila de mensagens/processos implementada pela classe MessageQueue.

A principal dificuldade encontrada pelos desenvolvedores ao criar processos em uma classe Thread ou Runnable é encontrar formas de enviar dados necessários entre as Threads. Para solucionar esse problema, um objeto da classe Handler oferece um canal único de comunicação com a Thread na qual o mesmo é instanciado. Outro ponto positivo da classe Handler é a possibilidade de reutilizar o objeto da classe Handler já existente em uma Activity e evitar assim que muitas instâncias sejam criadas.

Apesar de não possuir uma forma de implementação tão intuitiva, a classe Handler é comumente utilizada em casos no qual é necessário agendar a execução de objetos da classe Message ou Runnable. Outro caso no qual a classe Handler é utilizada é quando existe a necessidade de adicionar uma ação a uma fila de execução que não é a mesma da classe Handler. Para utilizá-la, é necessário criar uma classe que herde de Handler e que sobrescreva o método handleMessage(). Caso seja necessário agendar a execução de alguma tarefa para ser executada posteriormente, é possível fazer isso através de diversos métodos como post(), postDelayed(), sendMessage(), etc.

Utilizando AsyncTask

A fim de facilitar e possibilitar a utilização da UI Thread a partir de um processo em background, a classe AsyncTask foi disponibilizada a partir da API level 3. Com ela é possível processar, a partir do encapsulamento da criação e da sincronização com a UI Thread, as operações em background e retornar o resultado para a UI Thread sem a necessidade de manipular Threads e/ou Handlers. A classe AsyncTask é definida a partir de três parâmetros genéricos Params, Progress e Result, e quatro métodos onPreExecute(), doInBackground(), onProgressUpdate() e onPostResult().

A classe AsyncTask foi projetada para facilitar o desenvolvimento de tarefas simples (com duração de poucos segundos, no máximo) processadas em background. Além disso, ela possibilita informar o progresso da tarefa que está sendo executada e enviar o resultado obtido, quando existir, para a UI Thread de forma simples.

Contudo, assim como a Thread em Java, a AsyncTask não possui um método padrão para lidar automaticamente com alterações na aplicação como a mudança de orientação retrato para paisagem e vice-versa. Ou seja, caso a Activity na qual a AsyncTask está associada seja recriada, o desenvolvedor deve lidar com essa condição de forma manual. Uma solução comum é utilizar a AsyncTask associada a um Fragment que possua o método para que seu estado atual seja mantido mesmo com alterações de orientação.

Para utilizar a classe AsyncTask, é necessário criar uma classe que herde de AsyncTask. A subclasse deve definir três parâmetros (Params, Progress e Result), respectivamente, onde Params corresponde ao tipo de dado que será passado como parâmetro ao método doInBackground(), Progress corresponde ao tipo de dado que será enviado à UI Thread informando o progresso da tarefa e Result é o tipo de dado que será retornado do método doInBackground() para o método onPostExecute(). Desses métodos, o que deverá ser sobrescrito da classe pai é o método doInBackground(), o qual é responsável por realizar a tarefa em paralelo a UI Thread. No caso de existir a necessidade da tarefa retornar um resultado para a UI Thread, a subclasse deverá sobrescrever o método onPostExecute() da classe pai. É comum enviar também para a UI Thread o progresso da tarefa e este pode ser feito através do método onProgressUpdate(), o qual deverá ser sobrescrito.

Utilizando Loader

Mais recentemente, a partir da API level 11, foi introduzida a classe Loader. Ela permite realizar a leitura assíncrona de dados em uma Activity ou Fragmente de forma simples, monitorar o repositório de dados enviando os novos conteúdos assim que o repositório é alterado. A classe Loader também permite que os dados carregados não sejam perdidos com qualquer alteração na configuração da aplicação e que seja acessível a qualquer Activity ou Fragment da aplicação.

Importância do feedback em tempo de execução ao usuário

Quando duas ou mais pessoas interagem entre si, elas podem se entender através de um feedback que uma envia a outra. A relação usuário-aplicativo, apesar de predominantemente unilateral, ocorre de forma semelhante: o usuário interage com algum elemento da aplicação e para que o mesmo entenda que a aplicação recebeu o estímulo e irá responder a ele, mas que ainda não possui os valores necessários para gerar a resposta, o desenvolvedor deve criar formas de enviar um feedback ao usuário através da aplicação.

A opção mais comum de enviar um feedback ao usuário é através de uma Dialog. Esta opção consiste em mostrar ao usuário uma janela com uma barra de progresso, uma mensagem ou ambos informando que uma tarefa está sendo executada.

Além de ser a forma mais comum, a utilização de uma Dialog para enviar o feedback ao usuário é útil em casos onde há a necessidade de evitar a interação do usuário com qualquer outro componente disponível na tela sem que se trave ela.

No entanto, por travar a navegação do usuário, o feedback através de uma Dialog deve ser utilizado de forma consciente. É possível habilitar a possibilidade do usuário remover a Dialog através do toque no botão voltar do sistema operacional ou através do toque na área externa ao Dialog. Ambos os casos removem o feedback enviado ao usuário mesmo que a tarefa em background continue sendo executada.

A outra opção é enviar o feedback como uma inner view, ou seja, inflando o componente com o feedback em um componente já existente no layout da aplicação. Assim como no feedback via Dialog, o componente pode se resumir a uma barra de progressão e/ou uma mensagem informando ao usuário da existência de uma tarefa sendo processada em background sem que se trave a navegação do mesmo.

A vantagem de se utilizar o feedback através de uma inner view é possibilitar ao usuário a livre navegação mesmo que uma tarefa esteja rodando em background. Contudo, é necessário que o desenvolvedor crie formas de evitar que o usuário acesse áreas da aplicação que necessitem do resultado obtido pela tarefa executada em background. Caso contrário, a interação poderá resultar em uma exceção, sendo nesse caso o NullPointerException mais comumente obtido.

Explicando o web service

Para exemplificar o conceito dos componentes responsáveis pela execução de tarefas em background, será desenvolvida uma aplicação chamada Example. A aplicação irá consistir em uma única tela na qual será carregado uma lista de comentários. Estes comentários serão armazenados no servidor externo e carregados a partir de uma requisição feita ao web service. Também será possível gravar novos comentários enviando para o servidor externo o conteúdo inserido pelo usuário em um EditText. Todas as requisições deverão ser feitas definindo o tipo dos parâmetros como POST.

O web service está implementado com duas funcionalidades básicas: get() e post() e as duas realizam consultas em uma única tabela denominada FEED. Esta tabela possui quatro colunas: id, username, comment e log_date conforme mostra a Figura 3.

Figura 3. Colunas da tabela FEED

Quando a aplicação necessitar buscar os comentários no banco de dados externo, ela terá que realizar uma requisição passando como o parâmetro action o valor get. Nesta requisição, também será necessário enviar os parâmetros page e limit como pode ser visto na Listagem 1.

switch($_POST[''action'']) { case get: $page = $_POST[''page'']; $limit = $_POST[''limit'']; $feed = new Feed(); $feed->get($page, $limit); break; ...
Listagem 1. Requisição ao método get()

E quando a aplicação necessitar gravar um novo comentário no banco de dados externo, ela terá que realizar uma requisição passando como o parâmetro action o valor post. Nesta requisição, também será necessário enviar os parâmetros username, comment e log_date como pode ser visto na Listagem 2.

switch($_POST[''action'']) { ... case post: $username = $_POST[''username'']; $comment = $_POST[''comment'']; $log_date = $_POST[''log_date'']; $feed = new Feed(); $feed->post($username, $comment, $log_date); break; }
Listagem 2. Requisição ao método post()

Criando a aplicação

Para começar, será criada a aplicação através do wizard do Android Studio. A aplicação terá como pacote br.com.example.concurrence e a Activity principal será chamada MainActivity e esta fará referência ao layout activity_main.xml conforme mostra a Figura 4.

Figura 4. Interface Android Studio após criação da aplicação Example
Nota: A aplicação foi desenvolvida utilizando a IDE Android Studio. Caso o leitor esteja mais familiarizado com o Eclipse e preferir utilizá-lo, ao invés do Android Studio, não haverá qualquer perda de conteúdo ou funcionalidade.

Para exemplificar de forma adequada a utilização da classe AsyncTask, a aplicação irá realizar requisições para um serviço externo. Para que o sistema não bloqueie as requisições, é necessário adicionar a permissão de acesso à internet no manifest da aplicação. Contudo, a aplicação poderá fazer uma requisição no momento no qual a conexão com a internet não está disponível, então é necessário verificar se a conexão com a internet está ativa ou não. Para realizar a verificação da conexão, também é obrigatório adicionar a permissão de leitura do estado da rede do dispositivo ao manifest da aplicação como pode ser visto no código:

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

Com as permissões adicionadas ao manifest da aplicação, será possível realizar as requisições nos serviços externos e verificar a disponibilidade de conexão com a internet do dispositivo a fim de evitar erros de indisponibilidade de rede como pode ser visto na Listagem 3. Nas aplicações que consomem muitos dados de serviços externos, é preferível desconsiderar a possibilidade de uso da rede 3G para evitar consumo e cobranças desnecessárias ao usuário.

public NetworkState isOnline(Context context) { try { ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); NetworkInfo m3G = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); if (mWifi.isConnected()) return NetworkState.WiFi; else if (m3G.isConnected()) return NetworkState.GGG; } catch (Exception e) { } return NetworkState.None; }
Listagem 3. Método de verificação da disponibilidade de conexão com a internet

O método isOnline() será utilizado sempre que uma requisição precisar ser executada. O método irá retornar um valor enum de NetworkState que irá variar entre WiFi, None e GGG.

Nas permissões do manifest, o desenvolvedor poderá adicionar a permissão para verificar a disponibilidade apenas da conexão Wifi ao invés da conexão com a internet em geral alterando o android.permission.ACCESS_NETWORK_STATE por android.permission.ACCESS_WIFI_STATE. É útil, no entanto, tratar o possível tráfego intenso de dados como inviável no caso da disponibilidade única da conexão 3G.

Nota: O valor referente à conexão 3G em NetworkState foi atribuída como GGG porque em Java não é possível declarar variáveis que comecem com numeral.

Antes de dar início ao processo que será executado em paralelo e sincronizar o resultado obtido com a UI Thread, é importante padronizar a forma de enviar ao usuário o feedback da atividade. Para isso, será implementado uma interface na classe Request com os métodos de padronização das requisições como pode ser visto na Listagem 4.

public interface Callback { public void onStart(Class<?> executingClass); public void onSuccess(Class<?> executingClass, Object result); public void onError(Class<?> executingClass, String errorMessage); public void onCancel(Class<?> executingClass); }
Listagem 4. Interface de padronização dos métodos das requisições

A interface terá definido quatro métodos de sincronização com a UI Thread. Para enviar um feedback de que uma task está sendo executada, o método onStart() da instância da interface na subclasse de AsyncTask deverá ser chamado no método onPreExecute(). No caso da tarefa ser cancelada, seja pelo usuário ou pela própria aplicação, o método onCancel() da instância da interface deverá ser executado. Caso ocorra algum erro na requisição, ou a requisição seja realizada com sucesso, os métodos a serem chamados devem ser onError() ou onSuccess(), respectivamente. O melhor momento para realizar a sincronização com a UI Thread através dos três últimos métodos declarados da interface Callback é no método onPostExecute() da subclasse de AsyncTask.

O tipo dos parâmetros das requisições irá variar de acordo com o web service. Para esse exemplo, o web service aceitará apenas parâmetros passados como POST. Em Android, para realizar uma requisição HTTP, é necessário criar um objeto de HttpClient. Como o web service irá ler apenas parâmetros passados via POST, será necessário criar um objeto de HttpPost e adicionar o mapa de parâmetros e valores nele como pode ser visto na Listagem 5.

{... HttpPost httpPost; HttpContext localContext = new BasicHttpContext(); HttpParams httpParams = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParams, 15000); HttpConnectionParams.setSoTimeout(httpParams, 15000); HttpClient httpClient = new DefaultHttpClient(httpParams); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(); if (params != null) { for (String key : params.keySet()) { nameValuePairs.add(new BasicNameValuePair(key, String.valueOf(params.get(key)))); } } httpPost = new HttpPost(url.replaceAll("\\s", "_")); httpPost.addHeader("Accept-Encoding", "gzip"); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, "UTF-8")); ...}
Listagem 5. Criação e configuração do objeto para requisição

Após definir os objetos HttpClient e HttpPost, será obtida uma resposta a partir da execução da requisição feita pelo objeto de HttpClient. Essa resposta será lida por um objeto de BufferedReader e o conteúdo será armazenado em um objeto de StringBuilder. Após terminar a leitura, o objeto de BufferedReader é fechado e o método retornará um JSONObject, o qual será criado a partir do conteúdo armazenado no objeto de StringBuilder como pode ser visto na Listagem 6.

{... HttpResponse response = httpClient.execute(httpPost, localContext); Header contentEncoding = response.getFirstHeader("Content-Encoding"); InputStream instream = response.getEntity().getContent(); if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { instream = new GZIPInputStream(instream); } BufferedReader reader = new BufferedReader(new InputStreamReader(instream, "UTF-8"), BUFFER_SIZE_8K); StringBuilder json = new StringBuilder(); String line = reader.readLine(); while (line != null) { json.append(line); line = reader.readLine(); } reader.close(); return new JSONObject(json.toString()); }
Listagem 6. Leitura do conteúdo obtido a partir da requisição ao web service

O método post() da classe Request repassa diversas exceções para o método de chamada. Algumas delas: UnsupportedEncodingException, ClientProtocolException, IOException, JSONException, SocketTimeoutException, IllegalStateException, NullPointerException. É importante lembrar de adicionar as permissões de acesso à internet e da leitura de conectividade mencionadas anteriormente ao AndroidManifest.xml.

Criando as classes para montagem das requisições ao web service

Para facilitar a construção e manutenção das URLs e também dos parâmetros a serem indexados, é útil criar uma classe para realizar a construção dessas URLs como pode ser visto na Listagem 7 e do mapa dos parâmetros como pode ser visto na Listagem 8, este último servindo para o caso das requisições serem feitas a partir de parâmetros POST.

public class URLBuilder { private final String HOST = "https://www.limaogames.com.br/articles/example/methods.php"; public String buildHostUrl() { return HOST; }
Listagem 7. Corpo da classe URLBuilder

Para utilizar a classe URLBuilder, basta criar uma instância de URLBuilder e executar o método buildHostUrl(), o qual irá retornar a constante que representa o endereço no qual o serviço a ser consumido está hospedado. Com a criação de uma classe que sirva como construtor das URLs a serem consumidas, o desenvolvedor poderá agrupar a criação das URLs em um único local facilitando a manutenção dos mesmos.

public class ParamBuilder { private final String ACTION = "action"; private final String GET = "get"; private final String POST = "post"; private final String COMMENT = "comment"; private final String LIMIT = "limit"; private final String LOG_DATE = "log_date"; private final String USERNAME = "username"; public HashMap<String, Object> buildGetParams(int limit) { HashMap<String, Object> map = new HashMap<String, Object>(); map.put(ACTION, GET); map.put(LIMIT, limit); return map; } public HashMap<String, Object> buildPostParams(String username, long logDate, String comment) { HashMap<String, Object> map = new HashMap<String, Object>(); map.put(ACTION, POST); map.put(USERNAME, username); map.put(LOG_DATE, logDate); map.put(COMMENT, comment); return map; } }
Listagem 8. Corpo da classe ParamBuilder

Para utilizar a classe ParamBuilder, basta criar uma instância de ParamBuilder e executar o método desejado passando os parâmetros necessários. Por definição do web service, os valores devem ser passados como POST a fim de aumentar a segurança dos dados através da chamada de dois métodos, get() e post(). O método get(), ao ser chamado, deve receber como parâmetro um inteiro com o valor do limite de feeds a serem carregados. O método post() deve receber como parâmetros o nome do usuário como uma String, o instante em milissegundos da chamada do método como um long e o comentário como uma String, respectivamente.

Para o método get(), é importante que o desenvolvedor fique atento com o limite de nós a serem carregados. Para retornos com muitos nós, um limite muito alto de itens carregados pode acarretar um TimeoutException pelo excesso de dados carregados.

Criando o modelo Comment

Para montar a lista de feeds que será utilizada na aplicação, será construído um modelo chamado Comment, o qual terá como finalidade armazenar as informações resultantes da requisição. As variáveis, por padrão, serão declaradas como private como pode ser visto na Listagem 9 para serem acessadas a partir de get. Neste exemplo não serão implementados os métodos set, pois os valores do identificador do comentário, do tempo em milissegundos da inserção do comentário, da descrição e do nome do usuário, respectivamente, serão atribuídos exclusivamente pelo construtor da classe e não serão alteráveis.

public class Comment { private long id; private long logDate; private String comment; private String name; ...}
Listagem 9. Declaração das variáveis da classe modelo Comment

Serão definidos dois tipos de construtores para a classe Comment. O primeiro deles será utilizado para instanciar um objeto de Comment já existente, ou seja, lido a partir do serviço get como pode ser visto na Listagem 10 e outro construtor para instanciar um objeto recém-criado ao finalizar com sucesso o método de post como pode ser visto na Listagem 11.

public Comment(long id, String name, String comment, long logDate) { this.id = id; this.logDate = logDate; this.comment = comment; this.name = name; }
Listagem 10. Construtor da classe modelo Comment para instância já existente
public Comment(long id, Comment c) { this.id = id; this.name = c.getName(); this.comment = c.getComment(); this.logDate = c.getLogDate(); }
Listagem 11. Construtor da classe modelo Comment para instância recém-criada

O método get responsável por recuperar o Comment pode ser observado na Listagem 12.

public String getComment() { return comment; } public long getId() { return id; } public long getLogDate() { return logDate; } public String getName() { return name; } }
Listagem 12. Código dos métodos get() da classe Comment

Criando a tarefa de buscar feeds

A tarefa de buscar feeds deve ser executada em segundo plano para evitar que ocorra o travamento da aplicação. Para isso, o método irá herdar da classe AsyncTask. Para execução, será passado um vetor de inteiro que será lido no método doInBackground(). Neste exemplo, o progresso da tarefa não será repassado para a UI Thread, por isso, o segundo parâmetro será declarado como Void. O resultado enviado para o método onPostExecute() será a lista dos feeds que foram instanciados a partir de um JSON consumido do web service como pode ser visto no código:

public class GetTask extends AsyncTask> { ...}

Para realizar a comunicação com a UI Thread, será utilizada a interface Callback, a qual foi implementada na classe Request. O contexto da aplicação será útil para ter acesso aos recursos da aplicação como as strings declaradas no arquivo strings.xml, evitando a utilização de valores comuns a aplicação, mas declarados localmente e passíveis de alterações como pode ser visto na Listagem 13.

private Context context; private Request.Callback callback; public GetTask(Context context, Request.Callback callback) { this.context = context; this.callback = callback; }
Listagem 13. Declaração das variáveis e construtor da classe GetTask

Antes da tarefa iniciar, ela irá comunicar a UI Thread que está prestes a ser executada. Para isso, o método onStart() da interface Callback será executado passando como parâmetro o nome da classe que está para ser executada. Dessa forma, a UI Thread saberá qual tarefa está para ser iniciada e dará o devido feedback ao usuário como pode ser visto na Listagem 14.

@Override protected void onPreExecute() { super.onPreExecute(); callback.onStart(getClass()); }
Listagem 14. Código do método onPreExecute()

A classe GetTask é responsável por buscar os feeds gravados no banco de dados externo. Essa tarefa é realizada no método doInBackground() passando o limit de feeds que serão lidos como parâmetro. Uma instância da classe Request será criada e o método post() será executado passando dois parâmetros: a URL na qual o web service se encontra e o mapa de parâmetros. A resposta virá como uma instância de JSONObject ou nulo caso ocorra alguma exceção. A instância de JSONObject será traduzida em elementos que serão utilizados para instanciar novos objetos de Comment e assim adicioná-los a lista de feeds como pode ser visto na Listagem 15.

@Override protected List<Comment> doInBackground(Integer... params) { int limit = params[0]; List<Comment> comments = new ArrayList<Comment>(); try { JSONObject jsonObject = new Request().post(new URLBuilder().buildHostUrl(), new ParamBuilder().buildGetParams(limit)); JSONArray jsonArray = jsonObject.getJSONArray("List"); for (int i = 0; i < jsonArray.length(); i++) { JSONObject object = jsonArray.getJSONObject(i).optJSONObject("Feed"); Comment comment = new Comment(object.getInt("id"), object.getString("username"), object.getString("comment"), object.getLong("log_date")); comments.add(comment); } return comments; } catch (Exception e) { e.printStackTrace(); } return null; }
Listagem 15. Código do método doInBackground()

A construção do objeto JSONObject é feita no método post() da classe Request passando como parâmetros a URL do web service e o mapa de parâmetros. Após realizar toda a tarefa, é necessário sincronizar os dados com a UI Thread. No método onPostExecute() será realizada a sincronização e o resultado será passado através da interface instanciada no construtor da classe GetTask conforme pode ser visto na Listagem 16.

O método onPostExecute() é o responsável por retornar para a UI Thread o resultado da requisição realizada no método doInBackground(). Caso o resultado seja diferente de nulo, o método de sucesso onSuccess() será chamado. Caso contrário, o método de erro onError() é chamado. O tratamento, seja qual for o resultado, será realizado pela classe ao qual a interface está vinculada.

@Override protected void onPostExecute(List<Comment> result) { if (result != null) callback.onSuccess(getClass(), result); else callback.onError(getClass(), context.getString(R.string.error_get_comments)); }
Listagem 16. Método onPostExecute()

Criando a tarefa de salvar comentário

Para a tarefa de salvar um comentário no servidor externo, assim como a de buscar feeds, será criada uma subclasse de AsyncTask para ser executada em segundo plano a fim de evitar um possível fechamento da aplicação por parte do sistema operacional. Para salvar o novo comentário, será passado por parâmetro uma instância do mesmo com os valores de entrada inseridos pelo usuário o qual retornará um JSON com o valor do identificador da nova instância. O progresso da tarefa não será informado ao usuário e o resultado enviado para a UI Thread será a mesma instância de Comment, apenas com o identificador atualizado como pode ser visto no código:

public class PostTask extends AsyncTask<Comment, Void, Comment> { ...}

As variáveis, o construtor e o método onPreExecute() da classe PostTask serão idênticos ao da classe GetTask. Para a tarefa de salvar um comentário no servidor externo, assim como a de buscar feeds, estará definida uma variável do contexto da aplicação e uma interface de callback que serão instanciadas no construtor da classe. O método onPreExecute() informará a UI Thread que a tarefa está prestes a ser iniciada e que um feedback deve ser enviado ao usuário.

O método doInBackground() receberá um vetor de instâncias de Comment como pode ser visto na Listagem 17. A requisição será feita a partir do método post() da classe Request passando como parâmetros a URL do serviço e o mapa de parâmetros. Em seguida, o resultado obtido será lido e o valor do identificador será repassado para uma nova instância de Comment, a qual também contará com os valores já existentes da instância de Comment recebida por parâmetro em doInBackground().

@Override protected Comment doInBackground(Comment... params) { Comment comment = params[0]; try { JSONObject jsonObject = new Request().post(new URLBuilder().buildHostUrl(), new ParamBuilder().buildPostParams(comment.getName(), comment.getLogDate(), comment.getComment())); return new Comment(jsonObject.optJSONObject("Object").optJSONObject("Feed") .getInt("id"), comment); } catch (Exception e) { e.printStackTrace(); } return comment; }
Listagem 17. Código do método doInBackground()

Ao receber a instância de Comment, será verificado o identificador da instância. Caso o valor seja igual a -1, significa que algum erro ocorreu na inserção do comentário e por isso o valor do identificador não foi atualizado e o erro será propagado para a UI Thread. Caso contrário, será retornada a instância de Comment com o valor do identificador atualizado como pode ser visto na Listagem 18.

As classes GetTask e PostTask possuem referência para o contexto da aplicação como forma de evitar definir strings dentro do próprio código. Dessa forma se pode evitar que uma alteração na mensagem de erro enviada através da interface Request.Callback precise ser alterada internamente em cada classe. Com o contexto da aplicação é possível carregar o recurso de strings definida em strings.xml e apenas alterar a mensagem do respectivo item.

@Override protected void onPostExecute(Comment result) { if (result.getId() == -1) callback.onError(getClass(), context.getString(R.string.error_post_comment)); else callback.onSuccess(getClass(), result); }
Listagem 18. Código do método onPostExecute()

Criando o layout da aplicação

Para esse exemplo, serão implementados três layouts, um para a Activity principal, um para os itens da lista de feeds e um para a janela de diálogo que será utilizada para o usuário informar seu nome, o mesmo que será enviado em cada comentário que for feito através da aplicação.

A Activity principal terá um cabeçalho onde será informado ao usuário a quantidade de comentários enviados por ele. Em seguida haverá um bloco com uma barra de progresso e uma mensagem de espera para informar ao usuário que a tarefa GetTask está sendo executada. Este bloco ficará visível apenas quando a tarefa GetTask estiver sendo executada. Em seguida estará uma ListView na qual serão carregados o conteúdo de uma lista de feeds através de uma subclasse de BaseAdapter. Finalmente, o layout terá um rodapé com um campo de entrada (EditText) e um botão para salvar o comentário no web service como mostra a Listagem 19.

<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="2.5dip"> <TextView android:id="@+id/label_welcome" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout android:id="@+id/loading_get" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal" android:layout_margin="5dip"> <ProgressBar android:layout_width="32dip" android:layout_height="32dip" android:layout_marginRight="5dip"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/loading_get_comments"/> </LinearLayout> <ListView android:id="@+id/list_comments" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1"/> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="5dip"> <EditText android:id="@+id/input_comment" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginRight="5dip" android:inputType="text"/> <Button android:id="@+id/button_post" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button_post"/> </LinearLayout> </LinearLayout>
Listagem 19. Código do arquivo de layout activity_main.xml

Após criar o arquivo activity_main.xml, o resultado pode ser visto na Figura 5. Mesmo com os componentes carregados em apenas uma tela, foi possível criar um layout limpo e que respeitasse as duas premissas da aplicação: enviar e ler feeds de um web service.

Figura 5. Layout da Activity principal

Criando a classe PreferencesLoader

A aplicação do artigo armazenará duas informações do usuário que serão mostradas no cabeçalho da MainActivity sendo que uma delas também será utilizada na inserção de um novo comentário no banco de dados externo. Essas informações serão armazenadas utilizando a interface de armazenamento SharedPreferences. As operações com essa interface serão realizadas a partir da classe PreferencesLoader a qual será criada e que herda da classe AsyncTaskLoader como pode ser visto no código a seguir. Além disso, a classe PreferencesLoader também irá implementar a interface OnSharedPreferenceChanceListener a fim de ser notificada quando algum valor armazenado em SharedPreferences for alterado:

public class PreferencesLoader extends AsyncTaskLoader<SharedPreferences> implements SharedPreferences.OnSharedPreferenceChangeListener

Para que os valores inseridos ou atualizados em SharedPreferences sejam salvos, é necessário que os mesmos sejam aplicados a partir de uma instância de Editor como pode ser visto no código:

public static void persist(final SharedPreferences.Editor editor) { editor.apply(); }

O carregamento dos dados armazenados em SharedPreferences será feito em uma Thread secundária a partir do método loadInBackground(). O método retorna a instância de SharedPreferences ativa como pode ser visto no código a seguir:

public SharedPreferences loadInBackground() { prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); prefs.registerOnSharedPreferenceChangeListener(this); return (prefs); }

Ao ser iniciada a instância de PreferencesLoader, o método onStartLoading() será executado. Caso já exista uma instância ativa de SharedPreferences, o método entregará como resultado a instância ativa. Contudo, pode ser que alguma alteração na orientação do dispositivo reinicie a aplicação e com isso a instância de SharedPrerences seja perdida. Nesse caso, o método onStartLoading() irá forçar o carregamento da instância de SharedPreferences como pode ser visto na Listagem 20.

protected void onStartLoading() { if (prefs != null) { deliverResult(prefs); } if (takeContentChanged() || prefs == null) { forceLoad(); } }
Listagem 20. Código do método chamado ao iniciar PreferencesLoader

Criando a classe DAO

A classe DAO servirá para agrupar algumas funcionalidades utilizadas na aplicação como dos métodos get() e post() que podem ser vistos na Listagem 21. Contudo, antes de chamá-los, é necessário saber se a tarefa está sendo executada e para isso se pode verificar através dos métodos isGetTaskRunning() e isPostTaskRunning() conforme pode ser visto na Listagem 22.

public void get(Context context, Request.Callback callback, int limit) { if (request.isOnline(context).equals(Request.NetworkState.None)) callback.onError(getClass(), context.getString(R.string.error_get_comments)); else { getTask = new GetTask(context, callback); getTask.execute(limit); } } public void post(Context context, Request.Callback callback, Comment comment) { if (request.isOnline(context).equals(Request.NetworkState.None)) callback.onError(getClass(), context.getString(R.string.exception_no_network)); else { postTask = new PostTask(context, callback); postTask.execute(comment); } } Listagem 22. Métodos de verificação do estado das Tasks. public boolean isGetTaskRunning() { return getTask != null && getTask.getStatus() != AsyncTask.Status.FINISHED; } public boolean isPostTaskRunning() { return postTask != null && postTask.getStatus() != AsyncTask.Status.FINISHED; }
Listagem 21. Código dos métodos get() e post()

Criando a Activity principal

A classe MainActivity deverá implementar três interfaces de Callback, são elas: View.OnClickListener, Request.Callback e LoaderManager.LoaderCallbacks como pode ser visto no código a seguir:

public class MainActivity extends Activity implements Request.Callback, View.OnClickListener, LoaderManager.LoaderCallbacks<SharedPreferences>

A implementação dessas interfaces por parte da Activity MainActivity, de acordo com a definição da utilização de interfaces, irá permitir à classe MainActivity sobrescrever os métodos dessas interfaces como se herdasse de uma classe qualquer.

Para a interface Request.Callback, a MainActivity deve implementar os métodos onStart(), onCancel(), onSuccess() e onError() como pode ser visto na Listagem 23. Com esses métodos, as tarefas assíncronas poderão sincronizar com a UI Thread sem a necessidade de criar uma instância singleton de MainActivity. Essa prática costuma deixar o código mais elegante e, para casos onde uma tarefa possa ser vinculada a mais de uma Activity ou Fragment, evita a necessidade de verificação da existência de uma instância singleton para cada Activity ou Fragment, desde que os mesmos implementem a interface Request.Callback.

@Override public void onStart(Class<?> executingClass) { if (executingClass.equals(PostTask.class)) { pgLoading = new ProgressDialog(this); pgLoading.setMessage(getString(R.string.loading_post_comment)); pgLoading.setCancelable(false); pgLoading.setCanceledOnTouchOutside(false); pgLoading.show(); } if (executingClass.equals(GetTask.class)) { layoutLoading.setVisibility(View.VISIBLE); } } @Override public void onSuccess(Class<?> executingClass, Object result) { if (executingClass.equals(GetTask.class)) { layoutLoading.setVisibility(View.GONE); comments.addAll((List<Comment>) result); adapter.notifyDataSetChanged(); } if (executingClass.equals(PostTask.class)) { totalPosts += 1; labelWelcome.setText(getString(R.string.label_welcome, name, totalPosts)); inputComment.setText(""); comments.add(0,(Comment) result); adapter.notifyDataSetChanged(); if (pgLoading != null) pgLoading.dismiss(); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(MainActivity.this).edit(); editor.putInt(TOTAL_COMMENTS, totalPosts); PreferencesLoader.persist(editor); } } @Override public void onError(Class<?> executingClass, String errorMessage) { if (executingClass.equals(GetTask.class)) { layoutLoading.setVisibility(View.GONE); } if (executingClass.equals(PostTask.class)) { if (pgLoading != null) pgLoading.dismiss(); } Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); } @Override public void onCancel(Class<?> executingClass) { }
Listagem 23. Métodos sobrescritos da interface Request.Callbacks

Para a interface LoaderManager.LoaderCallbacks, a MainActivity deve sobrescrever os métodos onCreateLoader(), onLoadFinished() e onLoaderReset() como pode ser visto na Listagem 24. A implementação da interface LoaderManager.LoaderCallbacks irá permitir que a aplicação acesse as informações armazenadas em SharedPreferences em uma tarefa paralela, a qual irá notificar a MainActivity assim que as informações forem carregadas.

@Override public Loader<SharedPreferences> onCreateLoader(int id, Bundle bundle) { return (new PreferencesLoader(this)); } @Override public void onLoadFinished(Loader<SharedPreferences> sharedPreferencesLoader, SharedPreferences sharedPreferences) { totalPosts = sharedPreferences.getInt(TOTAL_COMMENTS, 0); name = sharedPreferences.getString(NAME, ""); if (name.equals("")) askName(); else labelWelcome.setText(getString(R.string.label_welcome, name, totalPosts)); } @Override public void onLoaderReset(Loader<SharedPreferences> sharedPreferencesLoader) { }
Listagem 24. Métodos sobrescritos da interface LoaderManager.LoaderCallbacks

Ao iniciar a Activity principal, há alguns pontos importantes para destacar no método onCreate(). Nele será vinculado o layout activity_main.xml a Activity em execução. Os objetos das views serão vinculados as views declaradas em activity_main.xml através do identificador das views. O LoaderManager será notificado para criar um novo loader caso não haja nenhum ativo e o método get() da classe DAO será executado caso ele ainda não esteja sendo executado como pode ser visto na Listagem 25.

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); buttonPost = (Button) findViewById(R.id.button_post); inputComment = (EditText) findViewById(R.id.input_comment); listComments = (ListView) findViewById(R.id.list_comments); labelWelcome = (TextView) findViewById(R.id.label_welcome); layoutLoading = (LinearLayout) findViewById(R.id.loading_get); buttonPost.setOnClickListener(this); getLoaderManager().initLoader(0, null, this); adapter = new CommentAdapter(this, comments); listComments.setAdapter(adapter); if (!commentDAO.isGetTaskRunning()) commentDAO.get(this, this, 50); }
Listagem 25. Código do método onCreate() da Activity principal

Ao carregar os dados de SharedPreferences, caso não exista um valor definido para NAME, o método askName() será chamado e nele será aberto uma Dialog na qual o usuário deverá informar um nome para continuar navegando na aplicação como pode ser visto na Listagem 26. Essa Dialog não poderá ser cancelada, sendo ela só finalizada após o usuário digitar um nome no campo de entrada.

private void askName() { final Dialog dialog = new Dialog(this); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setContentView(R.layout.dialog_edit_name); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); final EditText inputName = (EditText) dialog.findViewById(R.id.input_name); Button buttonSave = (Button) dialog.findViewById(R.id.button_save); buttonSave.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (!inputName.getText().toString().trim().equals("")) { name = inputName.getText().toString().trim(); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(MainActivity.this).edit(); editor.putString(NAME, name); editor.putInt(TOTAL_COMMENTS, totalPosts); PreferencesLoader.persist(editor); labelWelcome.setText(getString(R.string.label_welcome, name, totalPosts)); dialog.dismiss(); } else Toast.makeText(MainActivity.this, R.string.exception_empty_fields, Toast.LENGTH_LONG).show(); } }); dialog.show(); }
Listagem 26. Código do método askName()

Para enviar um novo comentário ao servidor externo, o campo de entrada deve ser preenchido e nenhuma outra instância de PostTask deve estar sendo executada como pode ser visto na Listagem 27. Caso as condições citadas não sejam implementadas, um único usuário poderá causar um grande transtorno no web service ao salvar feeds sem um comentário ou salvar diversos feeds iguais, os quais foram armazenados através de cliques consecutivos no botão de salvar e a consequente inicialização de múltiplas instâncias da classe PostTask.

private void post() { if (isCommentInputFilled()) { if (!commentDAO.isPostTaskRunning()) commentDAO.post(this, this, new Comment(-1, name, getCommentInputText(), System.currentTimeMillis())); } else Toast.makeText(this, R.string.exception_empty_fields, Toast.LENGTH_LONG).show(); }
Listagem 27. Código do método post()

Ao final do desenvolvimento a aplicação ficará com uma tela conforme mostra a Figura 6. A lista de feeds será populada ao iniciar a aplicação pelos últimos feeds armazenados no servidor externo. Uma pequena mensagem de boas-vindas será mostrada ao usuário com o nome e a quantidade de comentários que foram enviados por ele. E por último, teremos um espaço no qual o usuário poderá escrever um comentário e enviar para o web service ao clicar no botão Enviar.

Figura 6. Aplicativo em execução

A utilização de tarefas assíncronas na plataforma Android visa evitar que a aplicação fique inoperante e consequentemente seja finalizada pelo SO. Existem diversas maneiras de desenvolver tarefas que rodem em paralelo com a UI Thread, contudo há também cuidados a serem tomados como, por exemplo, a sincronização com a UI Thread.

Além de buscar alternativas para consumo de um web service ou leitura de dados armazenados internamente, o desenvolvedor pode implementar, com tarefas assíncronas, processos para agendar a execução de métodos em determinado horário ou caso uma condição seja satisfeita como o caso de Push Notifications, alarmes e download de conteúdo online.

Confira também

Artigos relacionados