Volley: Conexiones HTTP eficientes y configurables

En este artículo vamos a desgranar el funcionamiento de Volley en Android y veremos una forma de configurar y estructurar una aplicación android basada en el Navigation Component con Navigation Drawer que haga uso de Navigation + Volley para acceder a un servidor REST, que nos ofrecerá un conjunto de puntos de acceso o end-points con diferentes tipos de respuestas JSON (string, objects, arrays, objetos, array de objetos) que serán tratadas por la aplicación. Existen muchas formas de estructurar una aplicación con Volley, esta es una de ellas. Veamos….

Descripción general de Volley

Fuente: Android Developers. (Replicado y comentado aquí )

Volley es una biblioteca HTTP que facilita y agiliza el uso de redes en apps para Android. Volley está disponible en GitHub.

Volley ofrece los siguientes beneficios:

  • Programación automática de solicitudes de red: No vemos las interioridades de la conexión HTTP
  • Varias conexiones de red simultáneas: Podemos lanzar tantas como queramos, en los callback de respuesta podremos tratarlas cuando terminen cada una.
  • Almacenamiento de respuestas en caché y en disco transparentes con coherencia de caché en HTTP estándar. Permite almacenar en la cache la solicitud, como clave tendremos la url y como valor la respuesta del servidor. Se rige por el protocolo de cache vía las cabeceras cache-control de HTTP.
  • Compatibilidad con la priorización de solicitudes : Podemos establecer prioridades en nuestras solicitudes.
  • API de cancelación de solicitudes: permite cancelar una única solicitud, o bien establecer bloques o grupos de solicitudes para cancelar
  • Personalización sencilla, por ejemplo, de reintentos o retiradas: Además podemos definir clases genericas para el tratamiento de solicitudes.
  • Ordenamiento sólido que permite completar correctamente la IU con datos recuperados de forma asíncrona de la red: Vía los callbacks de respose y error que pueden ser definidos en el contexto del UI Thread y por tanto con acceso al UI.
  • Herramientas de depuración y rastreo

Volley se destaca por sus operaciones de tipo RPC que se usan para completar la IU, por ejemplo, obtener una página de resultados de la búsqueda como datos estructurados. Se integra fácilmente con cualquier protocolo y, además, incluye compatibilidad con strings sin procesar y JSON (objects o arrays). Al proporcionar compatibilidad integrada con las funciones que necesitas, Volley elimina la necesidad de escribir código estándar y te permite concentrarte en la lógica que es específica de tu app.

Volley no es adecuado para operaciones de transmisión o descarga grandes, ya que almacena todas las respuestas en la memoria durante el análisis. Para operaciones de descarga grandes, te recomendamos usar una alternativa como DownloadManager.

La biblioteca principal de Volley se desarrolla en GitHub y contiene la canalización principal de despacho de solicitudes, además de un conjunto de utilidades que pueden aplicarse de manera general disponible en la “caja de herramientas” de Volley. La manera más fácil de incorporar Volley en tu proyecto es agregar la siguiente dependencia al archivo build.gradle (Module app) de tu app:

    dependencies {
        ...
        implementation 'com.android.volley:volley:1.1.1'
    }
verified_user
Android Manifest

Para usar Volley, debes agregar el permiso android.permission.INTERNET en el manifiesto de tu app.
De lo contrario, tu app no podrá conectarse a la red.

Estructura Volley. Enviando solicitudes

Para enviar una solicitud, simplemente debes crearla y agregarla a la RequestQueue con add(), como se mostrará a continuación. Una vez que lo haces, la solicitud pasa por la canalización (un conjunto de pasos), se envía al servidor o se obtiene de la cache, se analiza la respuesta sin procesar (se comprueban los códigos de retorno no el contenido) y se entrega (al interfaz de usuario vía un callback).

La imagen muestra las distintas etapas de procesamiento de Volley.

Para enviar una solicitud a Volley hay que crearla, es decir crear la request que puede ser de varios tipos, string o JSON, por ejemplo. Una vez creada la request tenemos que enviarla a la RequestQueue. Esta cola de peticiones alberga las peticiones que van a ser enviadas al servidor o servidores. De extraer las peticiones de la cola y procesarlas se encarga Volley, nosotros sólo debemos colocarlas en la cola y esperar la respuesta en el callback que indiquemos.

Para poder enviar una Request a la RequestQueue, ésta primero debe haber sido creada. Podemos crear la RequestQueue en para procesar la petición en cualquier parte de nuestro código (al salir de ámbito, una vez sin peticiones pendientes, se destruirá) o bien podemos crear una clase especial (llamada de utilidad o singleton) que contenga la RequestQueue, de forma que si además esa clase de utilidad que contiene la cola la declaramos singleton (una única instancia de la misma) podemos hacerla accesible desde cualquier punto de nuestra aplicación. Lo veremos más adelante.

Una vez añadida la petición a la cola, Volley comprueba si la petición ha sido previamente almacenada en cache. Si está en cache, comprueba su fecha de expiración, si no ha expirado, entonces la puede servir sin realizar la solicitud real al servidor. La lee de la cache (Request read from cache and parsed) y la sirve al callback (Parsed response delivered on main thread).

El tema de la caché crea confusión, ¿que pasa si el servidor ha modificado los datos? ¿cómo sabe volley si el servidor ha modificado los datos?. Esto lo veremos más adelante cuando revisemos el protocolo de cache para HTTP, pero de momento decir, que el servidor, que es el que define cómo servir los datos (nosotros sólo los pedimos), asigna una caducidad a los mismos, puesto que sabe cuán variables van a ser. Por tanto la respuesta del servidor (en la cabecera de la response) viene con unos datos de expiración y con un Etag, o token de validación. Además indica si puede almacenarse en caché o no (privada en el navegador o pública en un CDN), y si no lo permite, mediante el mecanismo del token de validación (que veremos más adelante) existe la posibilidad de no tener que recrear toda la respuesta en el servidor si ya fue enviada y no han cambiado los datos.

Es decir, básicamente la response que recibimos tiene unos parámetros de configuración de cache, si podemos almacenar en cache lo haremos y sucesivas peticiones comprobarán la existencia en cache de la petición.

Como hemos dicho, si la petición puede ser servida de cache (Cache hit), no se envía al servidor y se lee de la cache. ¿Pero que pasa si el servidor puso un timeout y queremos volver a hacer la petición aunque no haya vencido el tiempo de permanencia en cache? Podemos limpiar la cache para que entonces volley no encuentre la petición en cache y no tenga más remedio que volver a realizar la petición al servidor. La nueva repuesta volverá a ser almacenada en cache, otra vez con su nueva fecha de expiración.

Cuando no se encuentra en cache (Cache miss) se lanza la petición al servidor y su respuesta, al igual que las que llegan desde cache, se servirá al callback que se haya dicho previa comprobación de los código HTTP de retorno de la request.

Vemos en el gráfico de la arquitectura de Volley que existen tres niveles de ejecución en tres threads, a nivel de UI (main thread) a nivel de cache (cache thread) y finalmente a nivel de red (network thread).

Una vez creada la RequestQueue llamamos a su método add(), con lo que Volley ejecuta el subproceso de procesamiento de caché y el conjunto de subprocesos de despacho de redes. Cuando agregas una solicitud a la cola, el subproceso de caché la toma y clasifica: si esta puede realizarse desde la caché, la respuesta almacenada en caché se analiza en el subproceso de caché y la respuesta analizada se entrega en el subproceso principal. Si la solicitud no puede realizarse desde la caché, se coloca en la cola de red. A continuación, el primer subproceso de red disponible toma la solicitud de la cola, realiza la transacción de HTTP, analiza la respuesta en el subproceso de trabajo, escribe la respuesta en la caché y devuelve la respuesta analizada al subproceso principal para su entrega.

Ten en cuenta que las operaciones costosas, como el bloqueo de I/O y el análisis o la decodificación, se realizan en subprocesos de trabajo. Puedes agregar una solicitud desde cualquier subproceso, pero las respuestas siempre se entregarán en el subproceso principal, en el callback de respuesta o error.

Cómo crear una RequestQueue ad hoc con newRequestQueue

Volley proporciona un método de conveniencia Volley.newRequestQueue, que configura una RequestQueue por ti mediante valores predeterminados y, luego, inicia la cola. Esta forma de crear la cola la podemos usar donde queramos, pero como hemos dicho antes, al salir de ámbito y quedar vacía la cola se destruirá (lo que es normal). Si queremos podemos funcionar así con Volley, pero veremos cómo definir un gestor Volley que mantenga una cola persistente y accesible desde cualquier parte de mi aplicación, por el momento veamos un ejemplo de uso con Volley.newRequestQueue (de Android Developers):

    final TextView textView = (TextView) findViewById(R.id.text);
    // ...

    // Instantiate the RequestQueue.
    RequestQueue queue = Volley.newRequestQueue(this);
    String url ="http://www.google.com";

    // Request a string response from the provided URL.
    StringRequest stringRequest = new StringRequest(Request.Method.GET, 
                url,
                new Response.Listener<String>() {
                       @Override
                       public void onResponse(String response) {
                          // Display the first 500 characters of the response string.
                          textView.setText("Response is: "+ response.substring(0,500));
                         }
                }, 
                new Response.ErrorListener() {
                       @Override
                       public void onErrorResponse(VolleyError error) {
                           textView.setText("That didn't work!");
                           }
                }
    );

    // Add the request to the RequestQueue.
    queue.add(stringRequest);

Como vemos usamos el método abstracto newRequestQueue pasándole el contexto (activity, fragment) con this. Esto crea una cola de peticiones.

Definimos entonces la url a la que vamos a lanzar la petición. Esta url será un parámetro de nuestra Request.

En este ejemplo realizamos después una petición de tipo StringRequest (como veremos hay más tipos). En el prototipo del constructor de la StringRequest pasamos 4 parámetros:

  • El método HTTP que va a llevar nuestra solicitud, en el ejemplo GET
  • La url de servicio
  • Un ResponseListener, o escuchador de respuesta que se crea in-line (new Response.Listener<String>) cuyos datos de respuesta indicamos que son <String>. Este Response listener tiene que implementar el método onResponse(String) (con el parámetro String en este ejemplo.
  • un ErrorListener, o escuchador de error, que también se crea in-line (new Response.Listener()). El Respose listener tiene que implementar el método onErrorResponse que recibe como parámetro un error Volley (VolleyError)

Una vez que tenemos la Request creada (stringRequest) , la podemos añadir a la cola con .add(stringRequest).

En este momento nuestro código continúa asíncronamente con la petición que Volley lanzará al servidor. Cuando esta petición sea respondida por el servidor se lanzará uno de los dos Callbacks que definimos en el momento de hacer la petición. Si la respuesta es correcta, se lanzará el callback onResponse pero si tiene algún error (código de error HTTP) se lanzará onErrorResponse.

Cómo cancelar una solicitud

Para cancelar una petición, hay que llamar al método Cancel() de nuestro objeto Request que hemos colocado en la cola. Una vez que la canceles, Volley garantizará que nunca se llame a tu controlador de respuestas, lo que en la práctica significa que puedes cancelar todas las solicitudes pendientes en el método onStop() de tu actividad y no tienes que llenar tus controladores de respuestas con verificaciones para getActivity() == null, en caso de que ya se haya llamado a onSaveInstanceState(), o bien a otro código estándar defensivo.

A fin de aprovechar este comportamiento, generalmente, deberías registrar todas las solicitudes en curso para poder cancelarlas en el momento adecuado. Existe una manera más fácil de hacerlo: puedes asociar una etiqueta con cada solicitud y, luego, usar esta etiqueta para definir el un grupo de solicitudes a cancelar. Por ejemplo, puedes etiquetar todas las solicitudes con el nombre de la Activity que las realiza y llamar a requestQueue.cancelAll(this) desde onStop() de la Activity.

Aquí tienes un ejemplo en el que se usa un valor de string para la etiqueta:

  1. Define tu etiqueta y agrégala a tus solicitudes.
    public static final String TAG = "MyTag";
    
    //Asumimos que en este punto existen tanto stringRequest como requestQueue

    // Set the tag on the request.
    stringRequest.setTag(TAG);

    // Add the request to the RequestQueue.
    requestQueue.add(stringRequest);

En el método onStop() de tu actividad, cancela todas las solicitudes que tengan esta etiqueta.

    @Override
    protected void onStop () {
        super.onStop();
        if (requestQueue != null) {
            requestQueue.cancelAll(TAG);
        }
    }
verified_user
Dependencia

Ten cuidado cuando canceles solicitudes.
Si dependes de tu la respuesta (en el callback) para avanzar a un estado o iniciar otro proceso,
deberás tenerlo en cuenta pues no se llamará al callback una vez cancelada la solicitud.

Cómo configurar una red y una caché

Anteriormente hemos utilizado el método Volley.newRequestQueue() para crear una cola de peticiones de manera rápida, que implementa todo lo necesario donde necesitemos. Podemos sin embargo crear una RequestQueue nosotros sin necesidad de usar ese método, de esta manera podemos configurar el protocolo de red subyacente o la cache (aunque normalmente utilizaremos los métodos por defecto).

Una RequestQueue necesita dos cosas a fin de realizar su trabajo: una red mediante la cual transportar las solicitudes y una caché para administrar el almacenamiento en caché. Hay implementaciones estándar para estas en la librería de Volley: DiskBasedCache proporciona una caché de un archivo por respuesta con un índice en memoria, y BasicNetwork proporciona transporte de red basado en tu cliente HTTP preferido.

BasicNetwork es la implementación de red predeterminada de Volley. Una BasicNetwork debe inicializarse con el cliente HTTP que tu app usa para conectarse a la red. Por lo general, es una HttpURLConnection.

En este fragmento, se muestran los pasos para configurar una RequestQueue:

    RequestQueue requestQueue;

    // Instantiate the cache
    Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB capacidad

    // Set up the network to use HttpURLConnection as the HTTP client.
    Network network = new BasicNetwork(new HurlStack());

    // Instantiate the RequestQueue with the cache and network.
    requestQueue = new RequestQueue(cache, network);

    // Start the queue
    requestQueue.start();

    // ---- Hasta aqui el código para definir la requestQueue con su transporte y cache.
    // ---- Desde aquó el código para realizar peticiones a dicha cola

    String url ="http://www.example.com";

    // Formulate the request and handle the response.
    StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
            new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            // Do something with the response
        }
    },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // Handle error
        }
    });

    // Add the request to the RequestQueue.
    requestQueue.add(stringRequest);

    // ...
    

Como vemos podemos configurar el tamaño de la cache y podríamos seleccionar otra librería de comunicaciones. El código que define nuestra cola de peticiones podríamos colocarlo en una clase de nuestro proyecto, de forma que no tuviéramos que repetirlo continuamente allá donde quisiéramos utilizarlo. El problema de esto es que cada vez que instanciemos un objeto de esa clase crearíamos una cola nueva. Esto no es problema si es lo que queremos hacer. Pero normalmente con una única cola de peticiones sería suficiente para una aplicación. ¿Cómo podemos crear una clase y que sólo se pueda crear un objeto de dicha clase? para poder tener sólo una clase. La respuesta es utilizar el esquema singleton para crear la clase.

El patrón singleton es sencillo. La propia clase crea (instancia) objeto de la propia clase que lo mantiene como dato miembro de la misma. El constructor de la clase es privado, pero se proporciona un método publico, GetInstance() por ejemplo, que nos devolverá la instancia que tiene guardada del objeto de la clase. Si es la primera vez que se llama a obtener instancia, entonces crea el objeto (con su constructor privado) y lo devuelve. Las siguientes veces ya no crea el objeto, simplemente devuelve el que ya tiene.

En la práctica, si solo necesitas realizar la solicitud una única vez (no necesitas clase, ni singleton, ni cambiar cache, ni nada…) entonces puedes crear la RequestQueue en donde sea que la necesites y llamar a stop() de la RequestQueue una vez que se muestre la respuesta o el error (en el callback correspondiente), como indicamos anteriormente mediante el método Volley.newRequestQueue().

Sin embargo, el caso práctico más común es crear la RequestQueue como singleton a fin de seguir ejecutándola durante el ciclo de vida de la app, como se describe en la próxima sección.

Cómo usar un patrón singleton

Si tu app usa la red con frecuencia, probablemente resulte más eficiente configurar una única instancia de RequestQueue que durará todo el ciclo de vida de la app. Hay varias maneras de hacerlo. El enfoque recomendado es implementar una clase singleton que encapsule la RequestQueue y otras funcionalidades de Volley. Otro enfoque es crear la subclase Application y configurar la RequestQueue en Application.onCreate(), aunque no se recomienda; un singleton estático puede proporcionar la misma funcionalidad de manera más modular.

verified_user
Patrón Singleton Volley

Implementando una clase singleton para gestionar nuestras peticiones nos permite
tener activa la RequestQueue durante todo el ciblo de vida de nuestra app.
La instancia de la RequestQueue deberá crearse en el contexto de la Application, no de la Activity.

Un concepto clave es que se deben crear instancias de la RequestQueue con el contexto de la Application y no de la Activity. De esta manera, se garantiza que la RequestQueue dure todo el ciclo de vida de la app, en lugar de volver a crearse cada vez que la actividad vuelva a generarse (por ejemplo, cuando el usuario rota el dispositivo).

Aquí tienes un ejemplo de una clase singleton que proporciona además la funcionalidad ImageLoader y la RequestQueue:

    public class MySingleton {
        private static MySingleton instance; //Objeto miembro que almacena una instancia de la misma clase
        private RequestQueue requestQueue;
        private ImageLoader imageLoader;
        private static Context ctx;

        //El constructor recibirá el contexto de la Aplicación
        private MySingleton(Context context) {
            ctx = context.getApplicationContext();
            requestQueue = getRequestQueue();

            //Creamos el image loader
            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);
                                               }
                                         });
        }

        //El método getInstance deberá recibir el contexto de la aplicación para llamar 
        //al constructor con el mismo..
        //Ademas se declara como syncrhonized para que no se pueda llamar desde dos sitios 
        //simultáneamente, garantizando que sólo se 
        //crea una instancia de la clase.
        public static synchronized MySingleton getInstance(Context context) {
            if (instance == null) {
                instance = new MySingleton(context);
            }
            return instance;
        }

        //Damos acceso a la RequestQueue
        public RequestQueue getRequestQueue() {
            if (requestQueue == null) {
                // getApplicationContext() is key, it keeps you from leaking the
                // Activity or BroadcastReceiver if someone passes one in.
                requestQueue = Volley.newRequestQueue(ctx.getApplicationContext());
            }
            return requestQueue;
        }

        //Permitimos añadir una petición de cualquier tipo de datos a la cola
        public <T> void addToRequestQueue(Request<T> req) {
            getRequestQueue().add(req);
        }

        //Damos acceso al imageloader
        public ImageLoader getImageLoader() {
            return imageLoader;
        }
    }

Aquí tienes algunos ejemplos de cómo realizar operaciones de RequestQueue con la clase singleton:

    // Get a RequestQueue
    RequestQueue queue = MySingleton.getInstance(this.getApplicationContext()).getRequestQueue();

    // ...

    // Add a request (in this example, called stringRequest) to your RequestQueue.
    MySingleton.getInstance(this).addToRequestQueue(stringRequest);
    

Descarga App


Contenido para usuarios registrados