sexta-feira, 29 de novembro de 2013

Comunicação HTTP eficiente no Android com Volley

Olá povo,

Conversando com meus novos mentores Neto Marin, Marcelo Quinta e Lúcio Maciel que conheci no DevBus Brasil, percebi uma unanimidade no que diz respeito a acesso HTTP no Android: a biblioteca Volley. Quando assisti o vídeo do Ficus Kirkpatrick no Google IO de 2013 não dei muita bola, mas os meus novos "orientadores" me mostraram o quanto essa biblioteca é poderosa e fácil de usar. Tudo isso aliado ao fato dela ser mantida pelo próprio Google.
Eu já falei em outros posts aqui do blog como fazer comunicação HTTP com AsyncTask (link 1 e link 2) e com AsyncTaskLoader (link), assim como carregar imagens da web em um Adapter usando o UniversalImageLoader (link). E também como ler um JSON de um WebService REST (link). Mas com o Volley podemos resolver todos os problemas citados nos posts anteriores e ainda remover todo aquele boiler plate que fazemos ao realizar essas requisições, simplificando o código. Além disso, ela tem as seguintes vantagens:
  • Comunicação paralela com possibilidade de priorização. Por exemplo, podemos criar uma fila de requisições JSON e outra de requisição de imagens e dizer que a primeira tem maior prioridade que a segunda;
  • Quando giramos a Activity, a mesma é destruída (mostrei como resolver isso aqui) e se nesse momento estiver havendo alguma requisição HTTP, o resultado é perdido. Podemos implementar algum tipo de cache em memória ou em banco, mas o Volley já faz esse serviço pra gente.
  • Um "ImageLoader" para carregar imagens da web, particularmente útil em adapters (falei de adapter aqui).
Depois dessa breve introdução, vamos por a mão na massa!

O Volley está em um repositório git no código-fonte do próprio Android. Então você terá que fazer um clone do mesmo via comando. Se você não tem o git instalado na sua máquina, siga esse tutorial aqui. Depois é só digitar no terminal.
git clone https://android.googlesource.com/platform/frameworks/volley

Feito isso, importe o projeto do Volley dentro do Eclipse e marque-o como biblioteca clicando com o botão direito sobre o projeto e selecionando Properties. Em seguida, selecione a opção Android do lado esquerdo e marque a checkbox "Is Library".

Em homenagem ao Ricardo Lecheta, vou fazer uma Activity que lê o JSON de carros disponível no site do seu livro de Android (que por sinal é muito bom e uso nas minhas aulas). E para ler o JSON e as imagens dos carros, vamos usar o Volley.

Vamos começar pela classe que vai manter a fila de execução de requisições do Volley bem como seu ImageLoader. O Google recomenda que ela seja um singleton, pois podemos gerenciar quantas filas de execução podemos ter e não termos que criar várias.
public class VolleySingleton {
  private static VolleySingleton mInstance = null;
  // Fila de execução
  private RequestQueue mRequestQueue;
  // Image Loader
  private ImageLoader mImageLoader;
 
  private VolleySingleton(Context context){
    mRequestQueue = Volley.newRequestQueue(context);

    mImageLoader = new ImageLoader(this.mRequestQueue, 
      new ImageLoader.ImageCache() {
        // Usando LRU (Last Recent Used) Cache
        private final LruCache<String, Bitmap> 
          mCache = new LruCache<String, Bitmap>(10);

        public void putBitmap(
          String url, Bitmap bitmap) {
          mCache.put(url, bitmap);
        }
        public Bitmap getBitmap(String url) {
          return mCache.get(url);
        }
      });
  }
 
  public static VolleySingleton getInstance(
    Context context){

    if(mInstance == null){
      mInstance = new VolleySingleton(context);
    }
    return mInstance;
  }
 
  public RequestQueue getRequestQueue(){
    return this.mRequestQueue;
  }
 
  public ImageLoader getImageLoader(){
    return this.mImageLoader;
  }
}
Vou definir um POJO simples que representa os objetos carro que iremos listar.
public  class Carro {
  String nome;
  String imageUrl;
 
  public Carro(String nome, String imageUrl) {
    this.nome = nome;
    this.imageUrl = imageUrl;
  }
}
Abaixo temos o arquivo de layout usado no adapter. Note que estamos usando NetworkImageView ao invés do ImageView tradicional.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/itemRoot"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" >

  <com.android.volley.toolbox.NetworkImageView
    android:id="@+id/img"
    android:layout_width="140dp"
    android:layout_height="70dp"
    android:src="@drawable/ic_launcher" />

  <TextView
    android:id="@+id/txtName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:textSize="20dp"
    android:text="@null"/>
</LinearLayout>
A classe Adapter listada abaixo herda de ArrayAdapter e está usando o NetworkImageView e o ImageLoader do Volley.
public class CarroAdapter extends ArrayAdapter<Carro>{

  static final int LAYOUT = R.layout.item_lista;
 
  public CarroAdapter(Context context, 
    List<Carro> objects) {

    super(context, LAYOUT, objects);
  }

  @Override
  public View getView(int position, 
    View convertView, ViewGroup parent) {

    Context ctx = parent.getContext();
    if (convertView == null){
      convertView = LayoutInflater.from(ctx)
        .inflate(R.layout.item_lista, null);
    }
    NetworkImageView img = (NetworkImageView)
      convertView.findViewById(R.id.img);
    TextView txt = (TextView)
      convertView.findViewById(R.id.txtName);
  
    Carro carro = getItem(position);
    txt.setText(carro.nome);
    img.setImageUrl(
      carro.imageUrl, 
      VolleySingleton.getInstance(
        getContext()).getImageLoader());
  
    return convertView;
  }
}
E finalmente a Activity... Ela implementa duas interfaces: Response.Listener e Response.ErrorListener. O método da primeira (onResponse) será chamada quando a requisição ocorrer sem problemas, e da segunda (onErrorResponse) caso contrário. No onCreate, obtemos a fila de execução a partir do nosso singleton, e depois criamos um JsonObjectRequest. No Volley, além desse tipo de Request, temos o ImageRequest e o StringRequest, o primeiro para imagens e o segundo para qualquer requisição que retorne uma String. Após criar a requisição, a adicionamos na fila para ser executada.
No método onResponse já recebemos o JSONObject, então é só fazer o parse do mesmo. Aqui poderíamos usar o Gson, mas eu não gosto dele :p
Após transformar o JSONObject em uma Lista de Carros, criamos e setamos o adapter da nossa ListActivity.
Se alguma coisa der errado, o método onErrorResponse será chamado, e daí estamos exibindo um Toast.
public class MainActivity extends ListActivity 
  implements 
    Response.Listener<JSONObject>, 
    Response.ErrorListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String url="http://www.livroandroid.com.br/livro/"+
      "carros/carros_classicos.json";
  
    RequestQueue queue = Volley.newRequestQueue(this);

    JsonObjectRequest jsObjRequest = 
      new JsonObjectRequest(
        Request.Method.GET, // Requisição via HTTP_GET
        url,   // url da requisição
        null,  // JSONObject a ser enviado via POST
        this,  // Response.Listener
        this); // Response.ErrorListener
  
    queue.add(jsObjRequest);  
  }

  @Override
  public void onResponse(JSONObject response) {
    List<Carro> carros = new ArrayList<Carro>();
  
    try {
      // Não precisamos converter o 
      // InputStream em String \o/
      JSONObject jsonCarros = 
        response.getJSONObject("carros");
      JSONArray jsonCarro = 
        jsonCarros.getJSONArray("carro");
   
      for (int i = 0; i < jsonCarro.length(); i++) {
        JSONObject jsonCarroItem = 
          jsonCarro.getJSONObject(i);
        String nome = 
          jsonCarroItem.getString("nome");
        String thumbnail = 
          jsonCarroItem.getString("url_foto");
    
        Carro carro = new Carro(nome, thumbnail);
        carros.add(carro);
      }
    } catch (Exception e){
      e.printStackTrace();
    }
   
    setListAdapter(new CarroAdapter(this, carros));
  }
 
  @Override
  public void onErrorResponse(VolleyError error) {
    Toast.makeText(this, "Erro!", 
      Toast.LENGTH_SHORT).show();
  }
}
Ah! Como toda app Android que acessa a Web, adicione a permissão de Internet no seu AndroidManifest.xml.

É isso, a partir de agora podemos utilizar o Volley em nossas aplicações com essa biblioteca usada e mantida pelo próprio Google. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

41 comentários:

Unknown disse...

Cara, não fazia ideia desta API. Estou desenvolvendo uma que tem algumas coisas parecidas.

Achei bem legal, simples e prática. Os caras do Google são f... mesmo! :-)

PedroGF disse...

Caso eu queira utilizar https, teria como com essa biblioteca também ?

Nelson Glauber disse...

Oi Pedro,

Nunca usei, mas acho que não tem problema. Afinal a maioria do serviços hoje trabalha com OAuth que é suportado pelo Volley.

4br4ç05,
nglauber

Anônimo disse...

Oi Nelson e sou novo em android e segui esse tutorial fiz igual so que na mainactivity o response.listener e o outro response fica pedindo p implementar as interface e na instacia jsonObjectRequest da erro

Nelson Glauber disse...

Oi Anônimo,

Se você observar, temos que implementar duas interfaces:
Response.Listener e Response.ErrorListener

Você tá fazendo isso...?

4br4ç05,
nglauber

Anônimo disse...

Olá Nelson!

esses seus programas estao disponiveis no github?

Nelson Glauber disse...

Oi Anônimo,

Não disponibilizei ainda códigos daqui do blog no github. Como sou professor tento fazer com que as pessoas que acessam o blog leiam, tentem fazer os exemplos dos post e remetam suas dúvidas. Acho que assim eles aprendem de verdade.
Normalmente todo o código está no post, então é só ler, entender, copiar e colar :)

4br4ç05,
nglauber

Jay Gatz disse...

Olá Nelson
Achei muito interessante o tutorial; não conhecia essa API e tinha um projeto extremamente parecido em que eu estava trabalhando. No entanto, não consegui resultado com o Volley: por algum motivo, o JSON que estava disponível para mim no site não era recuperado pelo método... tudo executa normalmente, mas a resposta da RequestQueue retorna nula...
Tentei alterar para String e também não obtive sucesso.
Ah, fora isso, uma outra dúvida: o método retorna um resultado do JSON se o mesmo não for identificado? Quero dizer, no trecho:


JSONObject jsonCarros = response.getJSONObject("carros");


eu teria resultado se eu pedisse um get sobre null (passando apenas o endereço)?
Hmm, escrevendo esse comentário agora que eu percebi... foi esse o erro, não é? O JSON não tinha identificação...
Ótimo tutorial o seu! Parabéns!

Ricardo Reis disse...

Olá Nelson

Como faço para implementar interfaces?

Ricardo Reis disse...

Nelson, ... Não sei sobre interfaces, o que são e como criar.
Poderia dar uma explicação?
Veja http://snag.gy/kfZvf.jpg

muito obrigado

Unknown disse...

estou com o mesmo problema do Ricardo

Nelson Glauber disse...

Oi Ricardo e Johnny,

Interface é um recurso da linguagem de programação Java (não do Android) que ajuda a criar famílias de classes sem quebrar a herança.
Aconselho dar uma olhada nesse apostila para entender melhor esse conceito do Java.
http://www.caelum.com.br/apostila-java-orientacao-objetos/interfaces/

4br4ç05,
nglauber

Tales Pimentel disse...

Olá Nelson,
Em CarroAdapter há um erro em item_lista daí troquei para o nome do xml do layout, por default, activity_main e os erros sumiram.
~> Acontece que o app "has stopped". Testei tanto no emulador quanto no smartphone Android 4.1
:(

Nelson Glauber disse...

Oi Tales,

Eu esqueci de colocar, mas o arquivo res/layout/item_lista.xml é o que fica logo após a classe Carro e é esse que você deve usar na classe CarroAdapter.

Se isso não resolver o problema, olha no logcat qual foi o erro, pois o "has stopped" acontece quando qualquer exceção não tratada é disparada pela aplicação.

4br4ç05,
nglauber

Anônimo disse...

Como as imagens são gerenciadas pela LIB? São Salvas onde? Como? Cache, Memoria?
Aguardo.

Nelson Glauber disse...

Oi Anônimo,

O código-fonte do volley é aberto, dá uma olhada lá. Assim você pode esclarecer todas as dúvidas.

4br4ç05,
nglauber

Rebualg disse...

Grande Glauber, seguinte fiz toda essa estrutura que tu fez. Até ai tudo bem, o problema é que a imagem ela não completa tamanho total... fica uma área em preto na imagem.

Exemplo, altura 100dp largura 150dp

praticamente só preenche 130dp de altura e preenche os 100dp altura

Nelson Glauber disse...

Oi Rebualg,

NetworkImageView é uma subclasse de ImageView, então você pode usar a propriedade android:scaleType para ajustar a imagem da maneira que desejar.

4br4ç05,
nglauber

P.S.: Belo nome o seu, mas ao contrário é melhor... :D

Nelson Glauber disse...

Oi Rebualg,

NetworkImageView é uma subclasse de ImageView, então você pode usar a propriedade android:scaleType para ajustar a imagem da maneira que desejar.

4br4ç05,
nglauber

P.S.: Belo nome o seu, mas ao contrário é melhor... :D

Maykel disse...

Olá, Nelson! Gostaria de saber se posso da mesma forma que consigo pegar os dados do web service, consigo enviar os dados de maneira facil, digo enviar Json e Imagem par ao servidor através da Volley...Muito obrigado!

Nelson Glauber disse...

Oi Maykel,

Quando você cria um objeto JsonObjectRequest o segundo parâmetro é um JSONObject que vc pode enviar para o servidor via POST.
Se você quer uma requisição com de JSON+Imagem dá uma olhada na seção MultiPart desse link aqui:
https://github.com/smanikandan14/Volley-demo

4br4ç05,
nglauber

Mychelle disse...

Olá Glauber, muito bom tutorial.

Bom é o seguinte, segui o seu tutorial, mas depois implementei o volley junto com gson, até ai tudo bem, inclusive testei isto que fiz com a url de carros que vpcê usou e funcionou muito bem. Mais tarde fui testar com um serviço rest que eu mesma implementei, é um serviço simples que retorna apenas uma lista de cliente, estou usando glassfish, mas não funcionou com o meu serviço, ocorre a seguinte exeption: 09-28 00:39:40.913: I/GoogleHttpClient(12660): Falling back to old SSLCertificateSocketFactory
09-28 00:39:41.443: I/ConfigFetchService(12660): fetch service done; releasing wakelock
09-28 00:39:41.443: I/ConfigFetchService(12660): stopping self
09-28 00:39:41.463: I/ConfigService(12720): onDestroy

O meu serviço funciona quando uso async mesmo e também funciona quando chamo a url pelo browser. Gostaria de saber o que é isso exatamente, pois já li e não entendi muito bem. Desde já agradeço.

Nelson Glauber disse...

Oi Mychelle,

Aparentemente é um problema com o certificado SSL do seu site que deve ter expirado. Você tá usando HTTPS? Dá pra usar HTTP?

4br4ç05,
nglauber

Unknown disse...

Nelson
LruCrache e somente a partir da versao 12?
caso queira para uma versao anteriror e possivel?

Nelson Glauber disse...

Oi Deiwson,

Tenta utilizar a classe android.support.v4.util.LruCache

4br4ç05,
nglauber

Leonardo Melo Santos disse...

Muito bom Glauber! Existe uma galera que gosta muito de usar Retrofit, mas esquecem que depender de reflection pode prejudicar o trabalho de obfuscação que o ProGuard faz. Portanto, prefiro utilizar Volley diante do Retrofit.

Mychelle disse...

Olá Glauber,

Gostaria de saber se o volley pode ser usado para uma grande quantidade de dados, por exemplo para uma grande quantidade Json ou Xml. Desde já agradeço.

Nelson Glauber disse...

Oi Mychelle,

Independente do Volley ou outra biblioteca, o ideal é que sua requisição não seja muito grande pois como sabemos as redes 3G/4G daqui não são muito boas.
Nesse caso, temos alguma opções como compactar (usando gzip) e/ou paginar os resultados (trazer N registros, depois mais N, ...).

4br4ç05,
nglauber

Mychelle disse...

Obrigada Glauber.

Unknown disse...

Olá Glauber, tudo bem?

Criei uma estrutura que utiliza um web service (PhP + volley) e grava informações no meu banco local e, posteriormente, se verificada que a conexão está OK, grava no banco de dados em um servidor (online).

As informações locais, possuem um status utilizando Android Support Annotations, conforme seu post recente (http://www.nglauber.com.br/2015/12/android-support-annotations.html). O status 0 é quando a info está local e o status 1 é quando a info está online.

Mesmo que eu tenha uma conexão OK, meu servidor pode estar indisponível e aí eu tenho um retorno através do onErrorResponse, correto?

Qual seria a melhor prática para atualizar o Annotations para 1 ? Poderia criar um método dentro do onResponse atualizando diretamente meu banco local ?

Obrigado.

Elias Neto

Nelson Glauber disse...

Olá,

É basicamente isso que você falou... Ao inserir um registro você marca-o como PENDENTE. Quando a comunicação com o servidor for bem sucedida (no onResponse), você atualiza a base local com um OK. Se houver alguma falha na comunicação, ele permanecerá com o status de PENDENTE...

4br4ç05,
nglauber

Unknown disse...

Olá Glauber,

Ainda com relação a atualização do status, tenho uma duvida sobre java seguindo o mesmo exemplo (se vc puder ajudar, ficarei grato).

Quando clico para gravar a informação, eu chamo o construtor da minha classe e passo os parâmetros (incluindo o PENDENTE). Antes de gravar no banco local, tento realizar o envio para o servidor. Se não gravar no servidor, atualizo (apenas para testes) como PENDENTE SERVIDOR. Se gravar no banco local, atualizo para OK.

Como o envio falha (deixei o servidor off propositalmente) eu quero fazer com que o objeto tenha o status alterado para PENDENTE SERVIDOR.

O meu problema até aqui é que não consigo atualizar o mesmo objeto. Ele altera o status (via debug eu visualizo isso), mas no momento que gravar no banco local (após tentar o server), ele passa o parâmetro como PENDENTE.

Há uma maneira de utilizar o mesmo objeto em todos os métodos desta Activity? Vi alguma coisa sobre "final", mas não consegui fazer funcionar.

Nelson Glauber disse...

Oi Elias,

Na minha opinião essa abordagem não é a melhor. Parta do presuposto que está errado e quando der tudo certo, você atualiza. Entendesse?
O que eu fiz no meu livro: um registro (no livro é um objeto Hotel) tem os status OK, INSERIR, ALTERAR e EXCLUIR.
Quando eu adiciono um registro, ele fica com o status INSERIR no banco local. Ao fazer uma requisição para salvar no servidor, se a aplicaçã mobile conseguir enviar o registro para o servidor, eu marco o registro no banco com OK. E repito esse processo para as operações de alterar e excluir.

4br4ç05,
nglauber

Unknown disse...

Entendi Glauber.

Perfeito. Montei desta forma e agora está OK.

Vou partir agora para a consulta das informações do servidor e posterior atualização no dispositivo.

Obrigado pelas dicas e feliz 2016.

SoftLuc - Sistemas disse...

Olá Professor, gostaria de saber se tem como isolar essas requisições do volley em um tipo de "DAO", onde eu passo o endereco de conexao e os dados do POST e a classe me retornaria o resultado das requisições?
Observei que as requisições estão sempre vinculadas as Activitys, mas não gostaria de ter que escrever uma nova requisição em cada Activity.

Ps.: Lembra do Projeto Proxaneta Recife?? kkkkkkkkkkkkkkkkkkkk

Nelson Glauber disse...

Oi SoftLuc,

O recomendado é isolar sim. A abordagem mais interessante seria utilizar um Service para não ter problemas com o ciclo de vida da Activity. Esse service armazenaria os dados localmente e a activity trabalharia apenas com esses dados. É essa abordagem que eu uso no meu livro.
Em relação ao Proxeneta... O melhor projeto de PGM de todos os tempos ;)

4br4ç05,
nglauber

SoftLuc - Sistemas disse...

:D

Unknown disse...

Boa noite, estou procurando uma maneira de fazer um aplicativo que busque dados de um webservice utilizando o volley, porém permita que esses dados sejam persistidos para que o usuário consiga acessar caso perca a conexão. Não é o caso de usar um SyncAdapter pois só quero persistir os dados consultados pelo usuário. Nesse caso eu deveria usar um ContentProvider e dentro dele fazer uma requisição usando o volley?

Nelson Glauber disse...

Oi Angelo,

Sem dúvida o SyncAdapter é uma ótima opção. Go ahead! Aconselho utilizar o OkHttp.

4br4ç05,
nglauber

Unknown disse...

Mas mesmo para trazer só os dados referentes a uma consulta do usuário o SyncAdapter é indicado?

Nelson Glauber disse...

Oi Angelo,

Você tem que avaliar cada caso. Nesse curso de Android do Udacity (https://www.udacity.com/course/developing-android-apps--ud853) eles criam uma aplicação que utiliza SyncAdapter para baixar informações de previsão do tempo dos próximos 15 dias e salva-las localmente. Essas informações são atualizadas frequentemente, e com SyncAdapter isso é feito em background sem que o usuário abra a aplicação necessariamente. Isso economiza bateria e dados.

Em resumo, é preciso conhecer o problema para ver a solução mais adequada.

4br4ç05,
nglauber