Realizar Peticiones Http Con La Librería Volley En Android

Volley es una librería desarrollada por Google para optimizar el envío de peticiones Http desde las aplicaciones Android hacia servidores externos.

Este componente actúa como una interfaz de alto nivel, liberando al programador de la administración de hilos y procesos tediosos de parsing, para permitir publicar fácilmente resultados en el hilo principal.

En este artículo veremos la guía completa para implementar Volley en una aplicación Android de ejemplo llamada MySocialMedia.

La aplicación que desarrollaremos se compone de una lista de ítems referentes a posteos realizados en un blog imaginario de Social Media, donde se implementará un ListView.

Aplicación Android con un ListView poblado con ayuda de la librería Volley

Descargar Código Del Proyecto Final

Si deseas desbloquear el link de descarga del proyecto en Android Studio sigue estas instrucciones:

¿Qué es y para qué sirve la librería Volley?

Como se dijo al inicio del articulo Volley es un cliente Http creado para facilitar la comunicación de red en las aplicaciones Android. A diferencia de la interfaz HttpURLConnection que vimos anteriormente, Volley está totalmente enfocado en las peticiones, evitando la creación de código repetitivo para manejar tareas asíncronas por cada petición o incluso para parsear los datos que vienen del flujo externo.

Lee también Operaciones HTTP en Android con el cliente HttpURLConnection

Entre sus características mas potenciadoras podemos encontrar:

  • Procesamiento concurrente de peticiones.
  • Priorización de las peticiones, lo que permite definir la preponderancia de cada petición.
  • Cancelación de peticiones, evitando la presentación de resultados no deseados en el hilo principal.
  • Gestión automática de trabajos en segundo plano, dejando de lado la implementación manual de un framework de hilos.
  • Implementación de caché en disco y memoria.
  • Capacidad de personalización de las peticiones.
  • Provee información detallada del estado y flujo de trabajo de las peticiones en la consola de depuración.

Evitar usar Volley en descargas de datos pesados: Aunque Volley posee ventajas enormes para el procesamiento de peticiones, no significa que se debe usar en cada petición que hagamos. Esta librería tiene limitaciones con la descarga de información demasiada extensa, ya que su operación se basa en salvaguardas en cache, lo que haría lento el proceso con datos voluminosos.

Volley posee varios componentes que optimizan la administración de las peticiones generadas desde las aplicaciones Android. La gestión comienza en una Cola de Peticiones que recibe cada una de las peticiones generadas, donde son previamente priorizadas para su realización.

Luego son seleccionadas por un elemento llamado Cache Dispatcher, cuya función es comprobar si la respuesta de la petición actual puede ser obtenida de resultados previos guardados en caché. Si es así, entonces se pasa a parsear la respuesta almacenada y luego se presenta al hilo principal. En caso negativo, se envía la petición a la Cola de Conexiones Pendientes, donde reposan todas aquellas peticiones que están por ejecutarse.

Luego entra en juego un componente llamado Network Dispatcher, el cual se encarga de seleccionar las peticiones pendientes de la cola, para realizar las respectivas transacciones Http hacia el servidor. Si es necesario, las respuesta de estas peticiones se guardan en caché, luego se parsean y finalmente se publican en el hilo principal.

Aunque esta es una definición básica, es muy concisa y significativa para comprender el proceso que realiza Volley al surgir peticiones. A continuación se muestra una ilustración que resume el flujo:

Diagrama de Arquitectura lógica de la librería Volley para Android
La imagen muestra los pasos descritos y además establece una subdivisión por líneas punteadas. La primer parte representa el hilo de UI en la aplicación, la parte intermedia es un hilo de procesamiento en caché que Volley crea para la gestión de cache y la tercera parte representa uno o varios hilos referentes al pool de conexiones realizadas para las peticiones.

Esta implementación elimina la implementación de hilos manualmente con Tareas asíncronas como era el caso del uso del cliente HttpURLConnection.

1. Añadir la librería Volley al proyecto Android Studio

1.1 Clonar Repositorio Git y añadir JAR

El primer paso a realizar es incluir Volley en el proyecto. Una de las formas de hacerlo es clonar el repositorio con la herramienta Git y luego construir un archivo .jar con base en este. La idea es ubicar el archivo en la carpeta libs, la cual permite referenciar librerías.

Librería jar en la carpeta libs de Android Studio
Puedes generar el jar con la herramienta jar del JDK de Java. Puedes encontrar una guía aquí:

http://docs.oracle.com/javase/tutorial/deployment/jar/build.html

También puedes usar el sistema de construcción Ant de Apache para generarlo o incluso el mismo Gradle.
Cuando ya esté listo, solo agrega la siguiente línea a tu archivo build.gradle y tendrás acceso a la librería:

compile files('libs/volley.jar')

1.2 Usar versión no oficial de Volley desde Maven Central

Nuestro amigo MCXIOAKE (usuario de Github) ha creado un mirror del repositorio original para Maven Central. Lo que significa que podemos obtener el .jar a través de una regla de de construcción externa en build.gradle. Para ello solo agrega la siguiente línea de dependencia:

compile 'com.mcxiaoke.volley:library:1.0.+'

El autor asegura que esta copia está en constante sincronización con el repositorio original de Google, por lo que no deberíamos preocuparnos por su actualización.

1.3 Importar los archivos fuente de Volley como un módulo

Importar como módulo los archivos de Volley es otro camino que se puede seguir. En este caso tendremos más cercanos los archivos de composición.

Para importar el nuevo módulo dirígete a File > Import New Module… y selecciona la ubicación de tus archivos.

Importar un Modulo en Android Studio
Seleccionar Carpeta del Módulo en Android Studio
Con ese movimiento la carpeta volley se construirá una sola vez, por lo que no debemos preocuparnos de trabajo extra de compilación. Para construir el modulo debes añadir a build.gradle la siguiente línea (asumiendo que nombraste al modulo :volley):

compile project(':volley')

Cuando se le dicta a Android Studio que importe un nuevo módulo automáticamente se añade al archivo settings.gradle una referencia del nuevo módulo, indicando que debe construirse un nuevo segmento.

include ':app', ':volley'

También podemos solo copiar y pegar los archivos fuente en la carpeta src como si se tratasen de código interno y luego ejecutar la construcción sin ningún problema.

2. Añadir el permiso para conexiones en el Android Manifiest

Por obvias razones debemos solicitar a Android que le permita a nuestra aplicación conectarse a la web, para ello añades la etiqueta <uses-permission> referente:

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

3. Diseñar Interfaz de la Aplicación Android

3.1. Diseñar layout de la actividad principal

Debido a que solo usaremos una lista en la interfaz, añadiremos como nodo raíz un ListView al layout de la siguiente forma:

<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/listView"/>

3.2 Diseñar aspecto de los ítems de la lista

El diseño se basa en un formato de fragmento enriquecido, donde se muestra una miniatura que presenta el post (ImageView), un encabezado (TextView Normal) y una pequeña metadescripción (TextView Small) asociada al contenido del artículo.
Veamos la definición del layout post.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:id="@+id/imagenPost"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="false"
        android:scaleType="centerCrop" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Medium Text"
        android:id="@+id/textoTitulo"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/imagenPost"
        android:textStyle="bold"
        android:layout_marginLeft="10dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Medium Text"
        android:id="@+id/textoDescripcion"
        android:layout_below="@+id/textoTitulo"
        android:layout_alignLeft="@+id/textoTitulo" />
</RelativeLayout>

3.3 Crear Adaptador personalizado para el ListView

Este paso ya lo hemos realizado una gran cantidad de veces, pero sin embargo para la implementación de Volley tenemos que dar un trato especial a la fuente de datos, que en este caso se encuentra en la web.

Crearemos un adaptador que extienda de la clase ArrayAdapter llamado PostAdapter, donde por el momento se dejará expresado en comentarios las acciones que se relacionen con Volley, las cuales se completarán a medida que entendamos las funcionalidades lógicas.

El adaptador quedaría de la siguiente forma:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;


public class PostAdapter extends ArrayAdapter {

    // Atributos
    private String URL_BASE = "http://servidorexterno.site90.com/datos";
    private static final String TAG= "PostAdapter";
    List<Post> items;

    public PostAdapter(Context context) {
        super(context,0);

        // Gestionar petición del archivo JSON
    }

    @Override
    public int getCount() {
        return items != null ? items.size() : 0;
    }

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

        LayoutInflater layoutInflater= LayoutInflater.from(parent.getContext());

        //Salvando la referencia del View de la fila
        View listItemView = convertView;

        //Comprobando si el View no existe
        if (null == convertView) {
            //Si no existe, entonces inflarlo
            listItemView = layoutInflater.inflate(
                    R.layout.post,
                    parent,
                    false);

            // Procesar item

        return listItemView;
    }
}

Por el momento existen 3 atributos. El string URL_BASE contiene la ubicación base de los archivos que accederemos, lo que significa que se debe concatenar esta cadena con la dirección relativa del recurso. Se declaró una constante TAG cuyo fin es gestionar actividades de log. Y también se encuentra una lista con elementos de tipo Post, la cual es una clase que encapsula los datos de los elementos de la lista.

3.4 Entender el origen de los datos del adaptador

La aplicación MySocialMedia obtiene la información de cada post desde un servidor externo a través de un archivo con formato JSON llamado social_media.json. Este archivo contiene un array de objetos con los atributos: titulo, descripción e imagen. Todos los atributos son de tipo string como se ve en el siguiente resumen (Verás el código expandido pero en realidad está minimizado).

{
 "items":
 [
        {
            "titulo": "Difusión de contenidos",
            "descripcion": "Aprende nuestra metodología para generar un plan de marketing de contenidos",
            "imagen": "/img/social1.png"
        }, {
            "titulo": "La Magia de las Campañas dirigidas",
            "descripcion": "¿Tus ventas están por el suelo?, entonces este articulo es para ti",
            "imagen": "/img/social2.png"
        }, {
            "titulo": "Posicionar tu marca con contenido audiovisual",
            "descripcion": "El objetivo de esta guía es desvelarte los secretos para el posicionamiento con videos ilustrativos",
            "imagen": "/img/social3.png"
        }, {
            "titulo": "¿Como Crear un Plan de Email Marketing?",
            "descripcion": "Descubre la importancia de capturar la información de aquellos usuarios que pueden ser tus futuros clientes",
            "imagen": "/img/social4.png"
        }, 
            ...
 ]
}

Como ves, las imágenes se representan con la ruta relativa dentro del servidor. De esta forma podremos realizar una petición hacia su ubicación concatenando con la url base.

Con esto en mente creamos la clase Post:

public class Post {
    // Atributos
    private String titulo;
    private String descripcion;
    private String imagen;

    public Post() {
    }

    public Post(String titulo, String descripcion, String imagen) {
        this.titulo = titulo;
        this.descripcion = descripcion;
        this.imagen = imagen;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getDescripcion() {
        return descripcion;
    }

    public void setDescripcion(String descripcion) {
        this.descripcion = descripcion;
    }

    public String getImagen() {
        return imagen;
    }


    public void setImagen(String imagen) {
        this.imagen = imagen;
    }
}

Teniendo en cuenta esta estructura, se puede deducir que a través de Volley debemos realizar una petición para obtener el archivo JSON. Luego, basados en los objetos obtenidos desde ese archivo, se debe realizar una serie de peticiones para obtener cada imagen alojada en el servidor.

Adicionalmente debe parsearse la respuesta JSON y el flujo binario de cada imagen para que nuestra aplicación pueda comprender dicha información.

4. Crear una nueva Cola de Peticiones

El uso de Volley comienza con creación de una cola de peticiones. La representación lógica de este elemento es la clase RequestQueue. Este objeto se encarga de gestionar automáticamente el envió de las peticiones, la administración de los hilos, la creación de la caché y la publicación de resultados en la UI.

Este elemento será parte de nuestro adaptador, por lo que lo añadiremos como atributo. Para crear una nueva instancia usaremos el método estático newRequestQueue() de la clase Volley:

...
private RequestQueue requestQueue;
...
// Crear nueva cola de peticiones
requestQueue= Volley.newRequestQueue(context);

Con esta declaración ya se tiene el camino abierto para añadir peticiones.

5. Generar y Añadir peticiones

Para realizar peticiones en Volley podemos acudir a ciertos tipos que ya están estandarizados para uso frecuente. Esto quiere decir que el desarrollador no debe tomarse tiempo en estructurar la petición, añadiendo hilos y generando procesos de parsing extenso.

Existen cuatro tipos de peticiones estándar:

  1. StringRequest: Este es el tipo más común, ya que permite solicitar un recurso con formato de texto plano, como son los documentos HTML.
  2. ImageRequest: Como su nombre lo indica, permite obtener un recurso gráfico alojado en un servidor externo.
  3. JsonObjectRequest: Obtiene una respuesta de tipo JSONObject a partir de un recurso con este formato.
  4. JsonArrayRequest: Obtiene como respuesta un objeto del tipo JSONArray a partir de un formato JSON.

5.1 Realizar petición para un archivo JSON

La primera petición que haremos será del tipo JsonObjectRequest, ya que tenemos un objeto JSON con un atributo llamado “ítems” de tipo array. El nombre del archivo es social_media.json.

Veamos la implementación:

// Nueva petición JSONObject
jsArrayRequest = new JsonObjectRequest(
        Request.Method.GET,
        URL_BASE + URL_JSON,
        null,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                items = parseJson(response);
                notifyDataSetChanged();
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG, "Error Respuesta en JSON: " + error.getMessage());

            }
        }
);

El primer parámetro del constructor es el método empleado en la petición. Como ves se usa la interfaz Method perteneciente a la clase Response, la cual contiene los métodos necesarios, como en este caso, donde se usa GET. Luego sigue la URL del recurso JSON, la cual se compone de dos strings concatenadas. El tercer parámetro son los pares clave-valor si se fuese a realizar una petición POST.

El cuarto parámetro es una clase anónima del tipo Response.Listener para definir una escucha que maneje los resultados de la petición.
Se debe sobrescribir el método onResponse() para codificar las acciones con la respuesta en el hilo de UI, en este caso parseamos el contenido del JSONObject y se le asigna el resultado al atributo ítems de nuestro adaptador.

Adicionalmente se le indica al adaptador que actualice su contenido con notifyDataSetChanged(), ya que no sabremos en qué momento la petición terminará exitosamente.

El quinto parámetro es una escucha que maneja los errores ocurridos en la transacción Http. Para ello se usa la clase Response.ErrorListener, la cual requiere dictaminar las acciones en su método onErrorResponse(). En este caso usamos el método estático d() de la clase Log para registrar en consola que ocurrió un error.

Finalmente usamos el método add() para añadir la petición a la cola de peticiones:

// Añadir petición a la cola
requestQueue.add(jsArrayRequest);

5.2 Realizar petición para las imágenes

En este situación usaremos el tipo ImageRequest para obtener cada imagen. Solo necesitamos concatenar la url absoluta que fue declarada como atributo, más la dirección relativa que cada imagen trae consigo en el objeto JSON:

// Petición para obtener la imagen
ImageRequest request = new ImageRequest(
        URL_BASE + item.getImagen(),
        new Response.Listener() {
            @Override
            public void onResponse(Bitmap bitmap) {
                Log.d(TAG, "ImageRequest completa");
                imagenPost.setImageBitmap(bitmap);
            }
        }, 0, 0, null,
        new Response.ErrorListener() {
            public void onErrorResponse(VolleyError error) {
                imagenPost.setImageResource(android.R.drawable.ic_lock_power_off);
                Log.d(TAG, "Error en ImageRequest");
            }
        });

// Añadir petición a la cola
requestQueue.add(request);

El código anterior es similar a la primera petición solo que esta vez usamos una escucha de tipo Listener<Bitmap>, la cual asignamos al ImageView del ítem procesado actualmente. En la escucha del error se asigna un drawable cuyo contenido representa visualmente un error de carga. Esto mostrará que las cosas no salieron como lo esperábamos.

El tercer, cuarto y quinto parámetros hacen referencia a una configuración adicional para la descarga de la imagen, pero ese tema no es necesario abordarlo debido al alcance de este artículo.

Como puedes notar, las peticiones con Volley no requieren el uso de tareas asíncronas, tampoco se necesita especificar que los resultados se deben publicar en el hilo UI y mucho menos parsear los flujos de información. La escucha Listener nos proporciona en onResponse() la respuesta completamente lista para usarse, ya sea un objeto JSONArray, un String o un Bitmap.

5.3 Acoplar peticiones Volley en el Adaptador personalizado

Una vez entendidas y creadas las peticiones, podemos completar nuestro adaptador PostAdapter de la siguiente manera, veamos:

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageRequest;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;


import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;


public class PostAdapter extends ArrayAdapter {

    // Atributos
    private RequestQueue requestQueue;
    JsonObjectRequest jsArrayRequest;
    private static final String URL_BASE = "http://servidorexterno.site90.com/datos";
    private static final String URL_JSON = "/social_media.json";
    private static final String TAG = "PostAdapter";
    List<Post> items;

    public PostAdapter(Context context) {
        super(context,0);

        // Crear nueva cola de peticiones
        requestQueue= Volley.newRequestQueue(context);

        // Nueva petición JSONObject
        jsArrayRequest = new JsonObjectRequest(
                Request.Method.GET,
                URL_BASE + URL_JSON,
                null,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        items = parseJson(response);
                        notifyDataSetChanged();
                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.d(TAG, "Error Respuesta en JSON: " + error.getMessage());

                    }
                }
        );

        // Añadir petición a la cola
        requestQueue.add(jsArrayRequest);
    }

    @Override
    public int getCount() {
        return items != null ? items.size() : 0;
    }

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

        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());

        // Referencia del view procesado
        View listItemView;

        //Comprobando si el View no existe
        listItemView = null == convertView ? layoutInflater.inflate(
                R.layout.post,
                parent,
                false) : convertView;


        // Obtener el item actual
        Post item = items.get(position);

        // Obtener Views
        TextView textoTitulo = (TextView) listItemView.
                findViewById(R.id.textoTitulo);
        TextView textoDescripcion = (TextView) listItemView.
                findViewById(R.id.textoDescripcion);
        final ImageView imagenPost = (ImageView) listItemView.
                findViewById(R.id.imagenPost);

        // Actualizar los Views
        textoTitulo.setText(item.getTitulo());
        textoDescripcion.setText(item.getDescripcion());

        // Petición para obtener la imagen
        ImageRequest request = new ImageRequest(
                URL_BASE + item.getImagen(),
                new Response.Listener<Bitmap>() {
                    @Override
                    public void onResponse(Bitmap bitmap) {
                        imagenPost.setImageBitmap(bitmap);
                    }
                }, 0, 0, null,null,
                new Response.ErrorListener() {
                    public void onErrorResponse(VolleyError error) {
                        imagenPost.setImageResource(R.drawable.error);
                        Log.d(TAG, "Error en respuesta Bitmap: "+ error.getMessage());
                    }
                });

        // Añadir petición a la cola
        requestQueue.add(request);


        return listItemView;
    }

    public List<Post> parseJson(JSONObject jsonObject){
        // Variables locales
        List<Post> posts = new ArrayList();
        JSONArray jsonArray= null;

        try {
            // Obtener el array del objeto
            jsonArray = jsonObject.getJSONArray("items");

            for(int i=0; i<jsonArray.length(); i++){

                try {
                    JSONObject objeto= jsonArray.getJSONObject(i);

                    Post post = new Post(
                            objeto.getString("titulo"),
                            objeto.getString("descripcion"),
                            objeto.getString("imagen"));


                    posts.add(post);

                } catch (JSONException e) {
                    Log.e(TAG, "Error de parsing: "+ e.getMessage());
                }
            }

        } catch (JSONException e) {
            e.printStackTrace();
        }


        return posts;
    }
}

5.4 Cancelar una petición

Si deseas evitar que una petición iniciada no publique su respuesta en el hilo principal usa el método cancel() del objeto Request, el cual cancela la visualización de los resultados obtenidos:

requestEjemplo.cancel();

También es posible cancelar un grupo de peticiones asociadas a una etiqueta impuesta. Para ello primero debes asignar la etiqueta a las peticiones elegidas con el método setTag(), el cual recibe un String que representa la etiqueta:

public static final String TAG = "Petición extenuante";
...

// Añadir etiqueta
requestEjemplo.setTag(TAG);
...

Y luego puedes usar el método cancelAll() de la cola de peticiones:

// Cancelar todas aquellas peticiones con dicha etiqueta.
if (mRequestQueue != null){
 requestQueque.cancelAll(TAG);
}

Con eso evitarás que todas las peticiones clasificadas por ese tipo publiquen resultados en la interfaz.

6. Crear Adaptador y asociarlo a la lista

Con toda la maquinaria creada es hora de poner en marcha nuestra aplicación, para ello crearemos una nueva instancia de PostAdapter y luego lo asociaremos a la instancia del ListView que tenemos en el layout de la actividad principal.

El código quedaría de la siguiente forma:

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;




public class MainActivity extends ActionBarActivity {

    // Atributos
    ListView listView;
    ArrayAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Obtener instancia de la lista
        listView= (ListView) findViewById(R.id.listView);

        // Crear y setear adaptador
        adapter = new PostAdapter(this);
        listView.setAdapter(adapter);

    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

Al correr la aplicación tendrías el siguiente resultado:

Captura de una aplicación construida con Volley

7. Características adicionales de Volley

7.1 Creación de patrón Singleton para alcance global dentro de la aplicación

MySocialMedia es una aplicación que usa las funcionalidades de Volley en solo una localidad, pero normalmente una aplicación basada en servicios web necesita realizar peticiones a lo largo de todas sus actividades y componentes. Esta situación haría que muchos programadores declararan colas de peticiones por todos lados o incluso pasar como parámetro la cola entre clases, lo cual llega  a ser repetitivo, poco eficiente y confuso.

Para desmontar este complejo enfoque, Google recomienda crear un Patrón Singleton que encapsule las funcionalidades necesarias de Volley. Recuerda que este patrón se caracteriza por limitar el alcance de la clase a un solo objeto, es decir, solo puede existir un solo objeto controlador que represente la existencia de la clase, restringiendo la instanciación de nuevos elementos.

Con esta solución, el singleton será omnipresente en todo el proyecto Android y se podrán usar las funcionalidades en cualquier lugar. Básicamente esta clase debe contener como atributo la cola de peticiones y el contexto de la aplicación (ojo, no el contexto de la actividad, ya que es necesario establecer independencia de la interfaz, por si en algún momento existe un cambio de configuración, como la rotación de pantalla).

Veamos la definición que se usaría para el proyecto actual:

import android.content.Context;
import android.graphics.Bitmap;
import android.util.LruCache;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;



public final class MySocialMediaSingleton {
    // Atributos
    private static MySocialMediaSingleton singleton;
    private RequestQueue requestQueue;
    private static Context context;

    private MySocialMediaSingleton(Context context) {
        MySocialMediaSingleton.context = context;
        requestQueue = getRequestQueue();        
    }

    public static synchronized MySocialMediaSingleton getInstance(Context context) {
        if (singleton == null) {
            singleton = new MySocialMediaSingleton(context);
        }
        return singleton;
    }

    public RequestQueue getRequestQueue() {
        if (requestQueue == null) {
            requestQueue = Volley.newRequestQueue(context.getApplicationContext());
        }
        return requestQueue;
    }

    public  void addToRequestQueue(Request req) {
        getRequestQueue().add(req);
    }

}

Como ves la clase está declarada como final para evitar la sobrescritura de métodos. Contiene un objeto reflejo singleton del mismo tipo para representar la única instancia de MySocialMediaSingleton. En cuanto al comportamiento, se crearon los métodos getInstance(), getRequestQueue() y addToRequestQueue().

getInstance() simplemente asigna memoria a la única instancia del singleton, donde se llama al constructor privado de la clase. Este método debe tener la propiedad synchronized, ya que la instancia será accedida desde varios hilos por lo que es necesario evitar bloqueos de acceso. El método getRequestQueue() obtiene la instancia de la cola de peticiones que se usará a través de toda la aplicación.

Para agregar un nueva petición debes acceder al método addToRequestQueue() una vez la instancia esté creada. Importantísimo aclarar que el parámetro del método getInstance() debe ser el contexto de la aplicación, esto protege la existencia de la cola de peticiones de cualquier afectación que sufra la actividad u otro componentes donde sea utilizado el singleton.

Veamos como añadir la petición JsonArrayRequest del adaptador a la cola:

...
JsonArrayRequest jsArrayRequest = new JsonArrayRequest(
        url + "/social_media.json",
        new Response.Listener<JSONArray>() {
            @Override
            public void onResponse(JSONArray response) {
                Log.d(TAG, "Respuesta Volley:" + response.toString());
                items = parseJson(response);
                notifyDataSetChanged();
            }


        },
        new Response.ErrorListener() {

            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG, "Error:" + error.getMessage());

            }
        });

// Añadir petición a la cola
<strong>MySocialMediaSingleton.getInstance(getContext()).addToRequestQueue(jsArrayRequest);</strong>
...

7.2 Optimizar la carga de múltiples imágenes con ImageLoader

La clase ImageLoader es un componente de Volley que agiliza la carga de múltiples imágenes provenientes de diferentes URL. El objetivo principal es eliminar el parpadeo que pueda llegar a producirse debido a la ubicación de la cache en disco. Esto se debe a que ImageLoader crea una caché especial en memoria para el proceso de descarga.

Para usar este potencializador, primero debemos incluir una instancia en nuestro Singleton.

...
private ImageLoader imageLoader;

private MySocialMediaSingleton(Context context) {
    ...

    imageLoader = new ImageLoader(requestQueue,
            new ImageLoader.ImageCache() {
                private final LruCache<String, Bitmap>
                        cache = new LruCache<String, Bitmap>(20);

                @Override
                public Bitmap getBitmap(String url) {
                    return cache.get(url);
                }

                @Override
                public void putBitmap(String url, Bitmap bitmap) {
                    cache.put(url, bitmap);
                }
            });
}
...

public ImageLoader getImageLoader() {
    return imageLoader;
}

Cuando se instancia el ImageLoader, se debe asociar la cola de peticiones correspondiente y un objeto del tipo ImageCache. Esta clase representa el acceso a la cache creada en memoria para almacenar las respuestas de las peticiones. Solo se debe sobrescribir anónimamente los métodos getBitmap() y putBitmap() basándose en un atributo del tipo LruCache.

La clase LruCache clase representa una cache gestionada por un algoritmo que prioriza a los elementos más usados. Cuando se instancia se especifica en sus parámetros de plantilla la relación entre pares String-Bitmap para referirse a las imágenes externas que se almacenaran en la porción de memoria. El parámetro entero que recibe en el constructor es el tamaño de la cache en unidades preestablecidas.

Puedes crear tu propia extensión de LruCacheimplementando la interfaz ImageCache, para personalizar la construcción de las unidades de memoria que se usarán en gestión de la caché.

El siguiente paso es llamar al método get() de ImageLoader en vez de usar una petición ImageRequest cada vez que deseemos obtener una imagen desde alguna URL.

Veamos cómo hacerlo para la imagen que se obtiene en el método getView() de PostAdapter:

// Obtener el image loader
ImageLoader imageLoader= MySocialMediaSingleton.getInstance(getContext()).getImageLoader();
// Petición
imageLoader.get(URL_BASE + item.getImagen(), ImageLoader.getImageListener(imagenPost,
         R.drawable.loading, R.drawable.error));

El primer paso fue obtener la instancia del Image Loader a través del Singleton con el método getImageLoader(). Con ese resultado fue posible llamar al método get(), el cual recibe tan solo dos parámetros: la URL de la imagen y un ImageListener. Este último objeto mencionado es una escucha que se construye a través del método estático getImageListener().

El método getImageListener() recibe la instancia del ImageView donde se almacenará la imagen, también recibe un drawable para visualizar un proceso de carga en caso que la petición demore un poco y otro drawable para representar un error en la respuesta de la petición.

7.3 Complementar a ImageLoader con NetworkImageView

Adicionalmente podemos incorporar el componente NetworkImageView para que trabaje en conjunto con el ImageLoader. Este view se puede usar en reemplazo del típico ImageView. La ventaja está en que el contenido de la imagen está directamente relacionado con la petición URL, lo que permite un manejo optimizado en la red y posibilitar cancelar la visualización en cualquier momento.

Para implementar el NetworkImageView en la definición XML, solo debes añadir la etiqueta com.android.volley.toolbox.NetworkImageView, veamos:

<com.android.volley.toolbox.NetworkImageView
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:id="@+id/imagenPost"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="false"
        android:scaleType="centerCrop" />

Lo siguiente es obtener la instancia del NetworkImageView y la del ImageLoader. Con esas referencias se llama al método setImageUrl() del view, el cual recibe la URL de la imagen y la instancia del ImageLoader. Con esa combinación la imagen se infla cuando sea necesario.

// Petición el image loader
ImageLoader imageLoader = MySocialMediaSingleton.getInstance(getContext()).getImageLoader();
// Petición
imagenPost.setImageUrl(URL_BASE +item.getImagen(), imageLoader);

Esta convención reduce en gran cantidad la codificación para la realizar una petición de la imagen.

7.4 ¿Cómo usar el método POST en la librería Volley?

Hasta ahora hemos visto cómo usar el método GET implícitamente a través de dos peticiones del tipo JsonArrayRequest e ImageRequest. Ambas se ejecutan con GET ya que están creadas solo para este método. En cambio las peticiones StringRequest y JsonObjectRequest si permiten usar el método POST debido a la simplicidad del formato de los datos.

A continuación se muestra un ejemplo de envío de los datos de un post hacia el servidor externo:

// Mapeo de los pares clave-valor
HashMap<String, String> parametros = new HashMap();
parametros.put("titulo", "Nuevo Post");
parametros.put("descripcion", "Nuevo Titulo");
parametros.put("imagen", "/img/nuevo_post.png");

JsonObjectRequest jsArrayRequest = new JsonObjectRequest(
        Request.Method.POST,
        URL_BASE + URL_COMPLEMENTO,
        new JSONObject(parametros),
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                // Manejo de la respuesta
                notifyDataSetChanged();
            }
        },
        new Response.ErrorListener() {

            @Override
            public void onErrorResponse(VolleyError error) {
                // Manejo de errores

            }
        });

Como ves el constructor de la petición recibe como primer parámetro el atributo POST de la interfaz Method. Los demás parámetros se configuran con las acciones que se desean aplicar en la respuesta y en la ocurrencia de errores. Ahora, para enviar los pares clave-valor en la petición existen dos caminos.

El primero fue usado en el anterior código, donde se creó un mapa para generar los pares que se enviarán. Este mapa se usa en el constructor de un objeto JSONObject que representa el cuerpo de la petición post.

La segunda forma consiste en crear una definición anónima en la instanciación de la petición, de tal manera que se sobrescriba el método getParams() para que retorne en los pares que deseamos.

JsonObjectRequest jsArrayRequest = new JsonObjectRequest(
        <strong>Request.Method.POST</strong>,
        URL_BASE + URL_COMPLEMENTO,
        <strong>null</strong>,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                // Manejo de la respuesta
                notifyDataSetChanged();
            }
        },
        new Response.ErrorListener() {

            @Override
            public void onErrorResponse(VolleyError error) {
                // Manejo de errores

            }
        })<strong>{
            @Override
            protected HashMap<String, String> getParams() {
                // Mapeo de los pares clave-valor
                HashMap<String, String> parametros = new HashMap<>();
                parametros.put("titulo", "Nuevo Post");
                parametros.put("descripcion", "Nuevo Titulo");
                parametros.put("imagen", "/img/nuevo_post.png");

                return parametros;
            }
        };</strong>

Este método retorna en un mapeado de aquellos pares que se enviarán hacia el servidor. En este caso se publican los atributos titulo, descripción e imagen con sus respectivos valores.

7.5 Crear peticiones personalizadas en Volley

Ya sabemos que podemos realizar cuatro tipos de peticiones previamente establecidos, pero en ocasiones el tipo de respuesta que deseamos no estará acorde a estas peticiones estándar. En estos casos debemos recurrir a crear peticiones personalizas con el framework de Volley que se acomoden a las necesidades presentadas.

Para ello debemos extender la clase Request<T>, la cual es una plantilla que requiere un tipo T referente a la respuesta que tendrá la petición. Por ejemplo, ImageRequest se extiende de la implementación Request<Bitmap>, debido a que después del parsing se obtiene una respuesta Bitmap.

La creación de una petición personalizada requiere que se sobrescriban los métodos abstractos parseNetworkResponse() y devilerResponse(), donde el primero debe contener la lógica de parsing del flujo de datos que se obtuvo de la petición y el segundo publica los resultados en el hilo UI. Es un mecanismo similar a los métodos doInBackground() y onPostExecute() de la clase AsyncTask.

Lee también Uso de Hilos y tareas asíncronas(AsyncTask) en Android

7.5.1 Crear una petición personalizada Volley con la librería Gson

¿Recuerdas la librería Gson?, espero que sí, ya que la usaremos para parsear de forma más cómoda los datos JSON de MySocialMedia. La idea es que de la petición Gson se obtengan como respuesta una lista de objetos Post listos para asignar.

Lee También Parsear datos JSON con la la librería GSON

El primer paso a realizar es crear una nueva clase que encapsule la lista de elementos Post que teníamos como atributo, ya que las colecciones de datos en la serialización Gson presenta inconvenientes por su definición de tipos. Así que encapsularemos la lista a manera de truco de la siguiente forma:

import java.util.List;

public class ListaPost {
    // Encapsulamiento de Posts
    private List<Post> items;

    public ListaPost(List<Post> items) {
        this.items = items;
    }

    public void setItems(List<Post> items) {
        this.items = items;
    }

    public List<Post> getItems() {
        return items;
    }
}

Crear la clase GsonRequest

El siguiente movimiento es crear la petición GsonRequest. Su objetivo es recibir cualquier tipo de objeto predefinido, en el cual se realizará un reflejo de parsing con los datos de un formato JSON. La siguiente composición del código es referencia de la documentación oficial de Android para la creación de peticiones personalizadas.

Veamos:

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.Response.ErrorListener;
import com.android.volley.Response.Listener;
import com.android.volley.toolbox.HttpHeaderParser;

import java.io.UnsupportedEncodingException;
import java.util.Map;


public class GsonRequest<T> extends Request<T> {
 // Atributos
    private final Gson gson = new Gson();
    private final Class<T> clazz;
    private final Map<String, String> headers;
    private final Listener<T> listener;

    /**
     * Se predefine para el uso de peticiones GET
     */
    public GsonRequest(String url, Class<T> clazz, Map<String, String> headers,
            Listener<T> listener, ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        this.clazz = clazz;
        this.headers = headers;
        this.listener = listener;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers != null ? headers : super.getHeaders();
    }

    @Override
    protected void deliverResponse(T response) {
        listener.onResponse(response);
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(
                    response.data, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(
                    gson.fromJson(json, clazz), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

Como ves, esta clase se construye en función de un tipo genérico T. Dentro de los atributos tenemos un objeto Gson para la implementación de la librería, un objeto clazz, el cual representa la clase del objeto en que se depositará la respuesta parseada. Un mapeado clave-valor para los headers de la petición (si es que usarás) y una escucha asociada al tipo T.

En el constructor solo se realizan las asignaciones correspondientes y se preconfigura la petición para que use el método GET. Debido a que la petición GsonRequest puede implementa headers, entonces se sobrescribe el método getHeaders() para su retorno.

Luego se sobrescribe deliverReponse(), donde es enviada la respuesta obtenida del parsing hacia el método onResponse().

En parseNetworkResponse() es donde se encuentra la lógica de parsing del flujo de datos. Recuerda que la petición realizada siempre trae los datos en forma de Stream, al cual se le debe dar un formato que entienda Java. En este caso se crea una cadena llamada json para asignar los datos de la respuesta con el atributo data. Además de eso añadimos al constructor la forma de codificación del flujo con parseCharset().

Este método estático recibe las cabeceras de la respuesta que envió el servidor y nos informa si es UTF-8, UTF-16, etc.

Luego se retorna el resultado del método success(), el cual entrega un resultado parseado y asociado a una caché. El primer parámetro es el resultado de fromJson(), donde se refleja el formato JSON del servidor en objetos del tipo clazz. El segundo usa el método estático parseCacheHeaders() para registrar una entrada de caché para la respuesta.

Instanciar la petición GsonRequest en el Adaptador

Finalmente y ya para terminar, vamos a reemplazar la petición JsonObjectRequest que teníamos por la nueva petición:

...
ListaPost items;

...
// Añadir petición GSON a la cola
MySocialMediaSingleton.getInstance(getContext()).addToRequestQueue(
        new GsonRequest<ListaPost>(
                URL_BASE + URL_JSON,
                ListaPost.class,
                null,
                new Response.Listener<ListaPost>(){
                    @Override
                    public void onResponse(ListaPost response) {
                        items = response;
                        notifyDataSetChanged();
                    }
                },
                new Response.ErrorListener(){
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.d(TAG, "Error Volley:"+ error.getMessage());
                    }
                }
        )

);

 

Conclusiones

Hemos visto gran cantidad de potencialidades que tiene la librería  Volley para ejecutar peticiones claras, cortas y rápidas. Es obvio que el alto nivel de su implementación la hace mejor opción que el uso de clientes como el HttpURLConnection.

También comprobamos que existen varias capacidades adaptativas de Volley para la personalización que se desee. Es cuestión de comprender la arquitectura de la librería para explotar sus mayores beneficios y posibilidades.