A comunicação assíncrona permite a troca de dados entre aplicações sem que elas fiquem aguardando o retorno da requisição para dar prosseguimento a suas atividades. O uso destas estratégias é essencial no desenvolvimento de apps que possibilitem uma boa experiência de uso para o usuário.
Uma tarefa comum nos aplicativos modernos é a intensa troca de dados com serviços web. Seja para persistir alguma informação na web ou para apresentar conteúdo sensível a localização e contexto. Tudo isso depende do bom e velho protocolo TCP/IP e sua lógica cliente/servidor.
Para dar mais ênfase à importância disso, basta lembrar ao leitor de que a rede de dados na maioria dos países do mundo ainda não está no mesmo ponto de qualidade que desejamos. Sendo assim, existe latência, pontos de intermitências e até mesmo as famosas sombras, regiões onde não existe conectividade. Neste caso, se uma requisição for feita, a resposta ao usuário pode demorar alguns longos segundos.
Para finalizar o cenário, imagine que a aplicação faz tudo de forma síncrona, ou seja, ao efetuar uma requisição HTTP e esperar pela resposta, a aplicação trava e simplesmente não deixa o usuário efetuar mais nenhuma operação. Qual o resultado disso?
- O usuário vai imaginar que a aplicação deu algum problema e clicar repetidamente em vários pontos da tela. Talvez, em um destes momentos a aplicação volte a responder a eventos e uma nova requisição será feita;
- O usuário pode simplesmente fechar a aplicação por imaginar que a mesma apresentou algum problema e parou de funcionar quando, na verdade, ela só está esperando a resposta do servidor de forma passiva;
- O usuário perde a paciência com sua aplicação e desinstala a mesma passando a usar sua concorrente.
Desta forma, é imprescindível construir aplicações Android onde todas requisições HTTP para persistir ou consumir dados de serviços web sejam feitas de forma assíncrona, não permitindo uma má impressão por parte do usuário.
Neste artigo será apresentada uma forma de fazer isso através da própria API do Android, além de alguns frameworks/APIs (Application Programming Interface) que tornam o trabalho do desenvolvedor muito rápido. Para completar, deixam o código limpo e totalmente separado em camadas, uma boa prática exigida pelo mercado para bons profissionais de programação.
Para consolidar os conceitos aqui apresentados, um aplicativo será construído para trocar informações com um serviço web criado com o Java Enterprise Edition e NetBeans.
Desenvolvendo o lado servidor
O componente servidor será simples, visto que o foco deste artigo é parte da requisição assíncrona da aplicação Android. Sendo assim, o container Java EE ficará responsável somente por dois Servlets Java. O primeiro chamado InsereCarro e o segundo RecuperaCarros. Além disso, uma classe de modelo de dados será criada com o nome de Carro. Para iniciamos o desenvolvimento, vejamos na Listagem 1 a definição da classe Carro.
public class Carro {
public String nome;
public int ano;
public String marca;
public float preco;
}
A classe contém apenas os atributos pertencentes ao objeto carro. Para manipular um objetivo do tipo Carro, criamos na Listagem 2 a classe InsereCarro.
Logo na primeira linha é encontrada uma anotação que só é possível na especificação do Servlet 3.0. Está sendo configurado o caminho /InsereCarro dentro do path da aplicação web para esta instância de servlet. Por esta razão, a URL para acionar este serviço será http://localhost:8080/artigo/InsereCarro?lista_de_parametros. Com esta anotação não é preciso editar o arquivo web.xml e fazer todos os mapeamentos de servlets com seus nomes e caminhos.
Na linha 2 temos a declaração da classe, sendo ela filha de HpptServlet. Somente através desta herança é possível a criação de um servlet. Na linha 4 foi criado um Vector que persistirá os objetos Carro.
Em seguida, na linha 6 temos o método processRequest que recebe como parâmetro uma instância de HttpServletRequest e uma instância de HttpServletResponse. Simplesmente são os objetos que transformam para o mundo Java a forma de funcionamento do protocolo TCP/IP: uma requisição feita pelo cliente e a resposta emitida pelo servidor.
O processRequest é chamado pelos métodos sobrescritos do Get e do Post. O GET e o POST são dois métodos do protocolo HTTP, mas existem outros, como PUT, DELETE, HEAD, TRACE, CONNECT, OPTIONS e PATCH.
Já dentro do método processRequest da linha 8 é configurado o tipo de conteúdo que o servidor emitirá como resposta ao pedido de requisição efetuado pelo cliente. Para tanto, foi utilizado o método setContentType.
Posteriormente, na sequência de linhas da 9 até a 12 são recuperados os parâmetros que são anexados à chamada da URL (http://localhost:8080/artigo/InsereCarro?nome=Fusca&marca=Wolks&ano=1982&preco=10000). A API do Servlet nos fornece o método getParameter para tal tarefa e nela definimos o nome do parâmetro. Nos tipos de dados int e float foram necessários os castings para o tipo correspondente visto que o retorno do método é uma String.
@WebServlet(urlPatterns = {"/InsereCarro"})
public class InsereCarro extends HttpServlet {
public static Vector<Carro> carrosSalvos = new Vector<Carro>();
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text;charset=UTF-8");
String nome = request.getParameter("nome");
int ano = Integer.parseInt(request.getParameter("ano"));
String marca = request.getParameter("marca");
float preco = Float.parseFloat(request.getParameter("preco"));
Carro carro = new Carro();
carro.nome = nome;
carro.ano = ano;
carro.marca = marca;
carro.preco = preco;
carrosSalvos.add(carro);
PrintWriter out = response.getWriter();
try {
out.println("{''status'':200}");
} finally {
out.close();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
Na sequência de linhas de 14 até 18 foi criado um objeto da classe Carro e seus atributos foram valorizados. Perceba que os valores atribuídos foram justamente aqueles recebidos por parâmetro na requisição. Na linha 19 este objeto é persistido dentro da instância de Vector.
Na linha 21 é criada uma instância de PrintWriter para podermos escrever a resposta ao cliente que enviou a requisição. O retorno sempre será um JSON informando sucesso, considerando que neste serviço web o código de status 200 represente sucesso. E na linha 25 o PrintWriter é fechado.
Na Listagem 3 é apresentada a classe RecuperaCarros. Esta classe basicamente permite que recuperemos um determinado objeto e retornemos seus atributos para a aplicação (linhas 10 a 19).
public class RecuperaCarros extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text;charset=UTF-8");
PrintWriter out = response.getWriter();
Vector<Carro> carros = InsereCarro.carrosSalvos;
out.println("{''carros'':[");
boolean first = true;
for (Carro carro : carros){
out.println(first?"{":",{");
out.println("''nome'':''"+carro.nome+"'',");
out.println("''marca'':''"+carro.marca+"'',");
out.println("''ano'':"+carro.ano+",");
out.println("''preco'':"+carro.preco+"");
out.println("}");
first = false;
}
out.println("]}");
out.close();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
Aplicação Android
A aplicação Android terá duas telas. A primeira mostra uma lista com os carros salvos no servidor. Para tanto, será necessário fazer uma requisição, recuperar o JSON, realizar o parse e, só depois, apresentar o conteúdo ao usuário, como apresenta a Figura 1.
Perceba a presença de dois menus na action bar. O primeiro, da esquerda para a direita, chamará a segunda tela que adiciona um novo carro no “banco de dados” localizado no servidor, sendo sua tela apresentada na Figura 2. O segundo menu faz um refresh na listagem de carros, buscando diretamente do servidor a lista completa de objetos Carro persistidos.
Os quatro campos apresentados ao usuário são exatamente os mesmos referentes aos atributos da classe Carro que está no servidor Java EE. Logo, na requisição serão recuperados os valores informados pelo usuário e encaminhados através do protocolo HTTP e seus métodos GET e POST.
Inicialmente, na Listagem 4 é apresentado o AndroidManifest.xml, que é coração da aplicação Android e que conta, dentre outras coisas, com as definidas das telas (Activities).
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="helloworld.teste.com.artigoandroidassincrono" >
<application
... >
<activity
android:name=".MainActivity"
android:label="Artigo" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".NovoCarroActivity"
android:label="Novo Carro"
android:parentActivityName=".MainActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
</application>
</manifest>
A grande maioria do conteúdo é aquela construído pelo próprio Android Studio no momento da criação do projeto. Nas linhas de 1 a 4 são definidas as principais propriedades das duas Activities da aplicação. No android:label, vale ressaltar que o mais indicado é criar um recurso de texto em res>values>string.xml.
Na linha 5 é definido o nome da Activity pai do NovoCarroActivity. Desta forma, o próprio sistema operacional já cria esta ligação entre as duas telas. Perceba que na Figura 2 existe uma seta à esquerda. Ao ser clicada, ela volta para a MainActivity sem precisarmos codificar este comportamento. Isso acontece por causa do uso do android:parentActivityName e do meta-data criado nas linhas subsequentes.
Já na Listagem 5 é apresentado o activity_main.xml, conteúdo visual da tela mostrada na Figura 1.
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
android:id="@+id/list">
</ListView>
Toda a interface se resume a um componente de lista (ListView) que ocupa a tela inteira (match_parent em layout_height e layout_width). Também foi configurado um espaçamento entre a borda do componente e o seu conteúdo para que o mesmo não fique colado nas laterais (padding). Por fim, o id desta View é list. Ele será recuperado posteriormente para popularmos os carros persistidos no servidor.
Na classe Java que compreende a tela propriamente dita (classe que estende de Activity ou uma de suas classes filhas) (Listagem 6), ainda não veremos grandes novidades. Posteriormente vamos codificar a requisição e leitura da resposta do servidor, trabalhando de diferentes formas, seja através da API Android ou de framework/libraries de terceiros.
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.miRefresh) {
return true;
} else if (id == R.id.miNovo) {
Intent intent = new Intent(this, NovoCarroActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
}
Os três primeiros métodos, onCreate, onCreateOptionsMenu e onOptionsItemSelected, fazem parte do ciclo de vida de uma Activity. No primeiro é anexado o recurso de layout, que foi mostrado na Listagem 5. No segundo é inflado um recurso de menu (que será visto na sequência) e construída a ActionBar e suas opções. Por fim, o terceiro método permite o tratamento de clique em um dos itens de menu.
Na linha 1 será codificado o tratamento para refresh dos dados da lista, ou seja, todo o processo de enviar uma requisição ao servidor, tratar a resposta, construir as instâncias de Carro e apresentá-las ao usuário. Nas linhas 2 e 3 é tratado o clique no botão de novo carro. Desta forma, é lançada uma Intent informando ao sistema operacional de que a Activity NovoCarroActivity deve ser apresentada ao usuário.
A Listagem 7 mostra o conteúdo do recurso activity_novo_carro.xml. A raiz desta interface é um gerenciador de layout chamado de LinearLayout. Ele organiza seus filhos em uma linha vertical ou horizontal. Por esta razão, na linha 1 é configurada a orientação do LinearLayout que para o nosso caso é vertical.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical" tools:context=
"helloworld.teste.com.artigoandroidassincrono.NovoCarroActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edtNome"
android:hint="Nome"/>
<EditText
android:hint="Ano"
android:inputType="number"
android:id="@+id/edtAno" />
<EditText
android:hint="Marca"
android:id="@+id/edtMarca" />
<EditText
android:hint="Preço"
android:inputType="numberSigned"
android:id="@+id/edtPreco" />
<Button
android:text="Salvar"
android:onClick="salvar"/>
</LinearLayout>
Logo na sequência definimos quatro EditText, que são campos de entrada de texto. Toda a View no android é uma região retangular e devemos fornecer a informação do tamanho da área que ela ocupará. Isso é feito com os atributos layout_weight e layout_height. Perceba que apenas no primeiro EditText eles estão presentes. Todos os outros EditText´s e o último Button contêm os mesmos valores para estas duas propriedades.
Na linha 4 é configurado um id para este primeiro EditText, assim como foi feito para os outros EditText´s também. Eles serão usados para recuperar estes elementos no código Java. Por ser um campo de entrada de texto e, pela natureza das informações de ano e preço, nas linhas 6 e 7 são definidos os tipos de dados de entrada aceitos pelos campos correspondentes.
Por fim, o Button acionará um método com o nome salvar, definido na propriedade onClick da mesma View (linha 9), e o texto foi configurado para Salvar, na linha 8.
A Listagem 8 mostra a classe NovoCarroActivity. Como pode ser visto, o código ainda é bem simples. Apenas uma classe que herda de ActionBarActivity e recupera os EditText através do método findViewById, atribuindo o retorno para as variáveis de classe com nomes homônimos aos ids dos elementos. E, talvez o mais importante, temos método salvar. Este será codificado posteriormente para fazer a requisição e enviar os dados do novo carro para o servidor.
public class NovoCarroActivity extends ActionBarActivity {
private EditText edtNome;
private EditText edtMarca;
private EditText edtAno;
private EditText edtPreco;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_novo_carro);
edtNome = (EditText) findViewById(R.id.edtNome);
edtMarca = (EditText) findViewById(R.id.edtMarca);
edtAno = (EditText) findViewById(R.id.edtAno);
edtPreco = (EditText) findViewById(R.id.edtPreco);
}
public void salvar (View v){
}
}
Tarefas assíncronas com AsyncTask
A primeira forma que iremos analisar para trabalhar com o protocolo HTTP de forma assíncrona é a classe AsyncTask, disponível na própria API do Android.
Vamos utilizar este conceito inicialmente na parte de refresh da lista, na primeira tela da aplicação. Para usar o AsyncTask é necessário criar uma subclasse da mesma. Através de Generics são passados os dados referentes a Params, Progress e Result. O primeiro serve como referência do tipo de dado que vai ser usado como parâmetro ao executar a tarefa assíncrona. O segundo é o tipo de dado que pode ser enviado da task no decorrer do trabalho. E por fim, o Result é a classe resultante no final da tarefa assíncrona. A Listagem 9 apresenta o primeiro esboço de sua utilização.
public class MainActivity extends ActionBarActivity {
...
class Tarefa extends AsyncTask<Void, Void, List<Carro>>{
@Override
protected List<Carro> doInBackground(Void... params) {
return null;
}
}
}
Ao criar uma classe filha de AsyncTask, no mínimo, é necessário sobrescrever o método doInBackground. Outro ponto crucial é o primeiro parâmetro deste método, que deve coincidir exatamente com o tipo passado no Params. Lembrando que os generics informados estão em ordem. Por fim, o retorno do método deve ser exatamente igual ao tipo informado no Result.
Dentro do método o programador não tem acesso à UI Thread, isso quer dizer que se o código tentar alterar qualquer View da aplicação, receberá um erro. Para facilitar a vida do desenvolvedor, a API fornece os métodos onPreExecute e onPostExecute. Eles são chamados exatamente antes e após a execução da tarefa assíncrona, respectivamente. Ambos têm acesso à UI Thread e podem trabalhar livremente com as Views.
Veja na Listagem 10 como a classe MainActivity ficou com o uso de tarefa assíncrona com AsyncTask.
public class MainActivity extends ActionBarActivity {
private ListView lView;
protected void onCreate(Bundle savedInstanceState) {
...
lView = (ListView) findViewById(R.id.list);
}
public boolean onOptionsItemSelected(MenuItem item) {
if (id == R.id.miRefresh) {
new Tarefa().execute();
return true;
}
}
class Tarefa extends AsyncTask<Void, Void, List<Carro>>{
private ProgressDialog dialog;
@Override
protected void onPreExecute() {
dialog = ProgressDialog.show(MainActivity.this, "Aviso", "Aguarde, buscando dados");
}
@Override
protected List<Carro> doInBackground(Void... params) {
List<Carro> carros = new ArrayList<Carro>();
try {
URL url = new URL("http://192.168.1.115:8080/artigo/RecuperaCarros");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
InputStream is = con.getInputStream();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(is));
StringBuffer sBuffer = new StringBuffer();
String line = "";
while ((line = reader.readLine()) != null) {
sBuffer.append(line);
}
JSONObject obj = new JSONObject(sBuffer.toString());
JSONArray array = obj.getJSONArray("carros");
for (int i = 0; i < array.length(); i++) {
JSONObject objCarro = array.getJSONObject(i);
Carro carro = new Carro();
carro.nome = objCarro.getString("nome");
carro.ano = objCarro.getInt("ano");
carro.marca = objCarro.getString("marca");
carro.preco = (float)objCarro.getDouble("preco");
carros.add(carro);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return carros;
}
@Override
protected void onPostExecute(List<Carro> carros) {
lView.setAdapter(new ArrayAdapter<Carro>(MainActivity.this,
android.R.layout.simple_list_item_1,
carros));
dialog.dismiss();
}
}
}
Na linha 1 é criado o objeto para o ListView que é recuperado na linha 2. Anteriormente, na resposta ao clique no item menu de refresh, não era efetuada nenhuma ação. Agora é instanciada a classe Tarefa (filha de AsyncTask) e, devido à herança, é chamado o método execute. Foi possível fazer esta chamada sem passar nenhum parâmetro porque na assinatura da classe usamos Void como Param.
Nesta implementação, onPre e onPostExecute estão na linha 10. Antes da tarefa assíncrona ser executada, um ProgressDialog é lançado (linha 11). O retorno do método show é armazenado no objeto dialog criado na linha 7. Desta forma, o usuário terá um feedback do que está acontecendo e não ficará preocupado.
Na tarefa assíncrona propriamente dita, que ocorre dentro do doInBackground, é iniciado um objeto List com o generic de Carro. Este será o objeto retornado ao final da tarefa. Na linha 18 é criada uma instância de URL, passando o endereço do Servlet como parâmetro. Devido às regras da linguagem Java teremos alguns try-catch neste trecho.
Na linha 19 é instanciado um HttpURLConnection, que serve para chamar um getInputStream (na linha 20) e recuperarmos esse streaming de dados oriundos da resposta do servidor. O trecho de linha de código que vai da linha 21 até a linha 29 serve para ler o retorno do servidor linha a linha.
Antes de prosseguir é bom relembrar que o RecuperaCarros retorna um dado em JSON. Na Figura 3 é apresentado de forma visual um esboço do retorno.
No formato de representação de dados JSON existem duas possibilidades: objetos e arrays. E a API do Android nos fornece uma classe JSONObject e a JSONArray justamente para permitir o parser de qualquer JSON.
Como a raiz do retorno é um objeto (linha 30), estamos passando os dados bufferizados para o construtor da classe JSONObject. Em seguida, a partir do objeto raiz, é requisitado o array identificado pela chave carros. Perceba o uso do método getJsonArray. Desta forma, o retorno será uma instância da classe JSONArray.
Neste ponto, estamos com um array populado com JSONObjects, sendo assim, a linha 32 cria uma estrutura de iteração para percorrer estes objetos. A cada laço é criada uma instância de carro e configurado o valor de seus atributos buscando as informações diretamente no objeto json parseado. Preste atenção no uso dos métodos get, isso é importante e faz toda a diferença na hora do parser.
Na linha 39 a instância é adicionada à lista de carros. A sequência de linhas de 41 até 51 são tratamentos de erros e fechamentos dos streamings de dados e possíveis conexões para evitar o uso de recursos desnecessariamente. Finalizando o método doInBackground, a lista é retornada.
Na linha 60 encontra-se o método onPostExecute, que será acionado exatamente após o return do doInBackground. Alguns detalhes merecem destaque aqui. O retorno do doInBackground deve ser exatamente igual ao parâmetro que o doPostExecute recebe. E ambos devem ser iguais ao Result, que foi configurado nos Generics do AsyncTask.
O corpo do método onPostExecute é bastante simples. A lista recebe um novo ArrayAdapter, que basicamente é um integrante do modelo MVC (Model View Controller) do sistema de UI do Android. Seu papel é ligar os dados à interface gráfica representada pela lista de objetos. E, por fim, o ProgressDialog é removido da visão do usuário pois o trabalho já foi feito.
Se tudo ocorreu bem, o leitor irá visualizar em seu aplicativo dois novos momentos. O primeiro, apresentado na Figura 4, mostra o ProgressDialog enquanto o trabalho está sendo feito de forma assíncrona. No segundo momento, ilustrado na Figura 5, a lista já foi alterada e mostra os carros que foram recebidos na resposta da requisição feita ao servidor.
Tarefas assíncronas com AQuery
No AsyncTask o leitor percebeu que o trabalho de requisição e tratamento da resposta ainda continua bastante manual. Até mesmo o parser JSON é feito quase que totalmente pelo desenvolvedor. Neste momento vamos apresentar a AQuery. Vamos estudar a Listagem 11 e ver como o código ficou usando este framework.
private ProgressDialog dialog;
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (id == R.id.miRefresh) {
dialog = ProgressDialog.show(MainActivity.this, "Aviso", "Aguarde, buscando dados");
AQuery aq = new AQuery(this);
String url = "http://192.168.1.115:8080/artigo/RecuperaCarros";
aq.ajax(url, JSONObject.class, retorno);
return true;
}
}
private AjaxCallback<JSONObject> retorno = new AjaxCallback<JSONObject>(){
@Override
public void callback(String url, JSONObject json, AjaxStatus status) {
if (json != null) {
List<Carro> carros = new ArrayList<Carro>();
try {
JSONArray array = json.getJSONArray("carros");
for (int i = 0; i < array.length(); i++) {
JSONObject objCarro = array.getJSONObject(i);
Carro carro = new Carro();
carro.nome = objCarro.getString("nome");
carro.ano = objCarro.getInt("ano");
carro.marca = objCarro.getString("marca");
carro.preco = (float) objCarro.getDouble("preco");
carros.add(carro);
}
lView.setAdapter(new ArrayAdapter<Carro>(MainActivity.this,
android.R.layout.simple_list_item_1,carros));
dialog.dismiss();
} catch (JSONException e) {}
} else {}
}
};
Na primeira linha temos o objeto ProgressDialog. Logo veremos porque a variável precisou virar de classe e não de método, como era anteriormente.
Na linha 4 começamos o tratamento do clique no menu item de refresh. Na linha 5 o diálogo de feedback é lançado, como já havíamos feito anteriormente. Na linha 6 inicializamos um objeto AQuery. Seu construtor exige uma instância de Context. Passamos this porque estamos em uma classe ActionBarActivity, que estende de Activity que, por sua vez, estende de Context.
A mesma URL que utilizamos antes é criada na variável String da linha 7. Na linha 8 o método ajax de AQuery é chamado. Ele possui diversas sobrecargas, mas vamos nos ater ao objetivo do artigo que é mostrar a requisição e tratamento da resposta HTTP de forma assíncrona.
Nesta sobrecarga do método são passados três parâmetros. O primeiro é a URL através de uma String. O segundo é o objeto Java que desejamos que o retorno seja parseado. A resposta já é automaticamente encapsulada em um JSONObject. Por fim, é passada uma instância de AjaxCallback que já foi criada como variável de classe na linha 13.
Esta classe permite a sobrecarga do método call-back que, como o próprio nome indica, será chamado automaticamente quando a resposta for tratada (com sucesso ou não) e já possuímos o objeto JSONObject pronto para uso (no caso de sucesso).
Dentro do método, o JSON será testado, e se ele for diferente de null, isso nos diz que tudo ocorreu sem problemas e temos a lista de carros já parseada.
A linha 17 cria um ArrayList de instâncias de objetos Carro que será passada como parâmetro na configuração do adapter da ListView. O trecho de código da linha 18 até a linha 28 trata o objeto JSONObject construindo as instâncias de Carro e configurando suas propriedades, exatamente como foi feito com AsyncTask.
Na linha 29 o método setAdapter de ListView é chamado e o diálogo é removido na linha 30. A sequência temos apenas catch e o else, que deveriam informar o usuário sobre possíveis problemas.
Caso você tenha tentado compilar este código, receberá um erro porque a classe AQuery não foi encontrada. Se o leitor estiver utilizando o Android Studio para a programação, basta adicionar o trecho apresentado na Listagem 12 nas dependências do Gradle.
dependencies {
compile fileTree(dir: ''libs'', include: [''*.jar''])
compile ''com.android.support:appcompat-v7:22.2.0''
compile ''com.googlecode.android-query:android-query:0.25.9''
}
Caso o leitor esteja usando o Eclipse ADT, visite o site oficial do AQuery e baixe diretamente o jar, colocando na pasta libs do seu projeto. No site oficial também são encontrados diversos exemplos de uso, além de requisições assíncronas. O leitor irá se surpreender com a redução da quantidade de código no seu projeto ao utilizar esta ferramenta.
Tarefas assíncronas com Volley
O framework Volley é uma biblioteca HTTP que torna o uso da rede no Android mais fácil e, principalmente, mais rápido. O Volley está disponível através do repositório aberto AOSP. Na Listagem 13 o exemplo foi reimplementado usando o Volley.
public class MainActivity extends ActionBarActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (id == R.id.miRefresh) {
dialog = ProgressDialog.show(MainActivity.this, "Aviso", "Aguarde, buscando dados");
fazRequisicao();
return true;
}
return super.onOptionsItemSelected(item);
}
public void fazRequisicao(){
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://172.16.50.240:8080/artigo/RecuperaCarros";
JsonObjectRequest jsonRequest = new JsonObjectRequest(
Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject json) {
if (json != null) {
List<Carro> carros = new ArrayList<Carro>();
try {
...
} catch (JSONException e) {}
} else {}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {}
}
);
queue.add(jsonRequest);
}
}
Na linha 7 o tratamento de clique no menu item de refresh não cria mais uma instância de AQuery, mas sim, faz uma chamada ao método fazRequisicao() da linha 13. Logo na linha 14 foi criada uma instância de RequestQueue através do método auxiliar newRequestQueue da classe Volley. O construtor recebe uma instância de Context.
O método newRequestQueue é um utilitário e torna o desenvolvimento mais rápido. Porém, no próprio site de desenvolvedores o indicado é utilizar o padrão Singleton para criar uma única instância de RequestQueue. Além de ser um processo caro computacionalmente, o mais indicado é fazer uma ligação com o contexto da aplicação como um todo, não com uma Activity em específico. Caso contrário, a cada reinicialização da Activity, o processo de criação desta fila se repetirá, onerando o sistema como um todo.
Na linha 17 é criada uma instância de JsonObjetRequest. A API fornece alguns tipos específicos de requisição: StringRequest, ImageRequest, JsonObjectRequest e JsonArrayRequest. Obviamente, dependendo do retorno do servidor, o desenvolvedor utiliza uma destas opções. No construtor da linha 17 foram utilizados os seguintes parâmetros:
- O método HTTP: GET, POST, PUT, dentre outros;
- Uma string contendo a URL do alvo da requisição HTTP;
- Um JsonRequest: como nossa requisição não envia nenhum parâmetro, foi posto um simples null;
- Um listener para tratar a resposta de sucesso da requisição assíncrona;
- Um listener para tratar o erro da requisição assíncrona.
Como utilizamos a classe JsonObjectRequest, o Listener de resposta para sucesso já recebe um objeto JsonObject. Esta interface também tem um método que deve ser sobrescrito de forma obrigatória, o onResponse (criado na linha 21). O código que vai da linha 22 até a linha 27 contém os três pontos na linha 25 porque é exatamente o mesmo tratamento do JsonObject visto no uso da AQuery. Até mesmo a parte de configuração do Adapter para a ListView.
Na linha 30 é configurado o tratamento ao listener de erro da requisição. Na linha 36, o método add da classe RequestQueue é acionado. Desta forma, esta requisição entra na fila gerenciada pelo próprio Volley e é acionada.
Tarefas assíncronas com Retrofit
No Retrofit o desenvolvedor se deparará com um pouco mais de código, mas isso não é de todo ruim porque a lógica de separação de camadas e responsabilidades estará bem clara, elevando o nível do código. A primeira mudança é no uso implícito do Gson, que é uma biblioteca que já faz o parser JSON automaticamente, levando em conta uma árvore de classes que o desenvolvedor passa para ele. As classes JsonObject e JsonArray usadas até aqui não serão mais úteis a partir de agora, pois o Retrofit utiliza o Gson e cuida disso.
Volte rapidamente até a Figura 3, o leitor verá que o objeto raiz do JSON é um objeto com um elemento só, identificado pelo nome carros, que é um array. No nosso código, até o presente momento, existe apenas uma classe Carro que seria a classe de cada um dos objetos deste array. Será preciso então criar uma nova classe que será chamada CarrosContaineer e é mostrada a seguir:
public class CarrosContainer {
public List<Carro> carros;
}
O próximo passo já será dentro da filosofia do Retrofit, que transforma seus endereços de requisições HTTTP, ou, também chamados de endpoints, em método dentro de uma interface Java. Além disso, são utilizadas exaustivamente as annotation para definir as propriedades desta requisição. Assim, será necessário criar uma interface que será chamada de CarrosService, como mostra a Listagem 14.
import retrofit.Callback;
import retrofit.http.GET;
import retrofit.http.Path;
public interface CarrosService {
1: @GET("/artigo/RecuperaCarros")
2: //List<Carro> listCarros(@Path("user") String user);
3: void listCarros(Callback<CarrosContainer> callback);
}
Logo na primeira linha utilizamos a anotação @GET indicando o uso deste método HTTP. Perceba que a listagem possui os imports necessários para cada anotação. Além disso, no próprio GET é configurado o path do endpoint que será requisitado.
A linha 3 cria o método listCarros que recebe um único parâmetro que é uma instância de Callback. Cuidado para usar a instância Callback da Retrofit. Este nome é comum e dentro da API Android existem dezenas de interfaces homônimas. Neste retorno da requisição é indicado que será retornada uma instância da classe CarrosContainer.
Por fim, na classe MainActivity apenas o método fazRequisicao sofrerá alterações, conforme podemos observar na Listagem 15.
public void fazRequisicao(){
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("http://172.16.50.240:8080")
.build();
CarrosService service = restAdapter.create(CarrosService.class);
service.listCarros(new Callback<CarrosContainer>(){
@Override
public void failure(RetrofitError error) {}
@Override
public void success(CarrosContainer c, retrofit.client.Response response) {
lView.setAdapter(new ArrayAdapter<Carro>(MainActivityWithRetrofit.this,
android.R.layout.simple_list_item_1,c.carros));
dialog.dismiss();
}
});
}
Na linha 1 é criada uma instância de RestAdapter passando configurações como o endpoint. Com o adaptador criado, basta chamar seu método create passando como parâmetro a interface do servisse, como vemos na linha 3. Com a instância de CarrosServices em mãos, basta chamar o método listCarros passando por parâmetro a implementação do Callback necessária. Neste momento estamos fazendo uso de uma classe interna anônima.
A interface Callback possui dois métodos que devem ser obrigatoriamente sobrescritos: o failure e o success, presente na linha 8. Ele já recebe uma instância de CarroContainer que contém uma propriedade carros que, por sua vez, é uma instância de List com objetos Carro. Então basta usar esta propriedade na linha 9 onde o adapter da ListView foi configurado.
Mas, onde está todo o parser JSON que fazíamos nas outras soluções? No Retrofit ele já entrega isso pronto.
Este artigo teve por objetivo mostrar a importância de pensarmos sempre em tarefas assíncronas. Isso deixa a aplicação em um outro nível e muda completamente o impacto de usabilidade para o usuário final.
Posteriormente foram apresentadas quatro soluções para requisições HTTP e tratamento da resposta subsequente, usando a própria API Android ou não. Independente da escolha do leitor, ficou claro que elas ajudam o desenvolvedor. Antes de mais nada, aceleram o desenvolvimento e, mais ainda, usam boas práticas de arquitetura e design de código.