- Utilizando AsyncTask de forma síncrona

Como hemos visto en este capítulo, Android impide que las operaciones con la red se realicen desde el hilo de la interfaz de usuario. Para saltarnos esta prohibición hemos desactivado StrictMode. Aunque lo más recomendable sería lanzar tareas asíncronas, en otros hilos, para acceder a la red.

Cuando intentamos aplicar esta segunda alternativa en Asteroides, aparece un problema: el método AlmacenPuntuaciones.listaPuntuaciones() ha sido diseñado para un uso síncrono; es decir, cuando se llama hay que esperar hasta que nos devuelvan el resultado, no siendo posible que el método vuelva inmediatamente, con lo que se deja la tarea pendiente. Posiblemente, habría sido interesante diseñar esta interfaz para trabajar de forma asíncrona. Por ejemplo, añadiendo el método comienzaDescargaPuntuaciones(), que arranca una tarea para la descarga y devuelve inmediatamente el control sin devolver nada. Desde esta tarea se podría lanzar un evento cuando se dispusiera de las puntuaciones.

En este ejercicio no vamos a cambiar el diseño de esta interfaz. En lugar de ello, vamos a introducir un AsyncTask dentro de listaPuntuaciones() y no retornaremos de este método hasta que termine y ya tengamos las puntuaciones. El siguiente esquema muestra este planteamiento:

 

Trabajando de esta manera realizamos el acceso a la red desde un hilo secundario. Por lo tanto, StrictMode no se quejará. Pero es muy importante que entiendas que realmente no hemos resuelto nada. Esta forma de trabajar bloquea igualmente el hilo de la interfaz de usuario. Es decir, un método execute() seguido de un get() es equivalente a llamar la tarea de una forma síncrona.

Entonces, ¿qué ventaja tiene usar un AsyncTask? En el método get() vamos a poder fijar un tiempo máximo a la tarea, por ejemplo get(4, TimeUnit.SECONDS). Pasado este tiempo, informamos al usuario de que el servidor no responde y continuamos.

Ejercicio: Uso síncrono de AsyncTask para acceso al servicio web PHP
              de puntuaciones

1.    En el proyecto Asteroides, copia la clase AlmacenPuntuacionesSW_PHP en una nueva clase y llámala AlmacenPuntuacionesSW_PHP_AsyncTask.

2.   Introduce las siguientes líneas al comienzo de la nueva clase, reemplazando el método listaPuntuaciones():

private Context contexto;

public AlmacenPuntuacionesSW_PHP_AsyncTask(Context contexto) {
   this.contexto = contexto;
}

public Vector<String> listaPuntuaciones(int cantidad) {
   try {
      TareaLista tarea = new TareaLista();
      tarea.execute(cantidad);
      return tarea.get(4, TimeUnit.SECONDS);
   } catch (TimeoutException e) {
      Toast.makeText(contexto, "Tiempo excedido al conectar",
            Toast.LENGTH_LONG).show();
   } catch (CancellationException e) {
      Toast.makeText(contexto, "Error al conectar con servidor",
            Toast.LENGTH_LONG).show();
   } catch (Exception e) {
      Toast.makeText(contexto, "Error con tarea asíncrona",
            Toast.LENGTH_LONG).show();
   }
   return new Vector<String>();
}

private class TareaLista extends AsyncTask<Integer, Void, Vector<String>>{
   @Override
   protected Vector<String> doInBackground(Integer... cantidad){
      //Copia el código que antes estaba en listaPuntuaciones()
   }
}

Empezamos añadiendo un constructor a la clase para indicar el contexto donde se ejecuta. Esto nos permitirá introducir Toast() en la clase.

Para obtener la lista de puntuaciones desde un nuevo hilo se ha creado un descendiente de AsyncTask que toma como parámetro de entrada un entero con la cantidad máxima de puntuaciones a obtener y nos devuelve un vector de String. El código de la tarea a realizar es casi idéntico al usado en el ejercicio anterior. Por esta razón no se ha incluido. En el siguiente punto se indica lo único que tendrás que cambiar. Para usar esta nueva clase se ha instanciado el objeto tarea.

En listaPuntuaciones() comenzamos instanciando un objeto de la clase TareaLista. El método execute() es utilizado para pasar los parámetros de entrada y arrancar la tarea. El método get() espera a que la tarea concluya y nos devuelve su salida. Como se ha comentado en el capítulo 5, hay que usar este método con cuidado, dado que bloquea el hilo de la interfaz de usuario. Sin embargo, dado que mientras estamos esperando la respuesta del servidor el usuario no puede realizar ninguna interacción, no va a suponer ningún problema. Para asegurarnos de que no nos quedamos bloqueados un tiempo excesivo, usamos una de las sobrecargas del método get(), que nos permite indicar el tiempo máximo a esperar y la unidad en que medimos este tiempo. En caso de sobrepasar este tiempo, se generará una excepción TimeoutException, que se procesa en la sección catch. También se recoge la posibilidad de que ocurra una excepción de cancelación de tarea. Al final del ejercicio se añade el método adecuado para cancelar si ocurre algún tipo de error.

3.   Reemplaza el código + "?max="+cantidad); por + "?max="+cantidad[0]);. Aunque esta tarea solo necesita un entero, la mecánica de AsyncTask hace que se nos pase un array de enteros.

4.   Introduce las siguientes líneas, reemplazando el método guardarPuntuacion():

public void guardarPuntuacion(int puntos, String nombre, long fecha){
   try {
      TareaGuardar tarea = new TareaGuardar();
      tarea.execute(String.valueOf(puntos), nombre,
            String.valueOf(fecha));
      tarea.get(4, TimeUnit.SECONDS);
   } catch (TimeoutException e) {
      Toast.makeText(contexto, "Tiempo excedido al conectar",
            Toast.LENGTH_LONG).show();
   } catch (CancellationException e) {
      Toast.makeText(contexto, "Error al conectar con servidor",
            Toast.LENGTH_LONG).show();
   } catch (Exception e) {
      Toast.makeText(contexto, "Error con tarea asíncrona",
            Toast.LENGTH_LONG).show();
   }
}

private class TareaGuardar extends AsyncTask<String, Void, Void> {
   @Override
   protected Void doInBackground(String... param) {
      try {
         URL url = new URL(
                  "http://jtomas.hostinazo.com/puntuaciones/nueva.php"
                  + "?puntos=" + param[0] + "&nombre="
                  + URLEncoder.encode(param[1], "UTF-8")
                  + "&fecha=" + param[2]);
         //Copia el código que antes estaba en guardarPuntuaciones
         return null;
   }
}

La mecánica para llamar al servicio web que almacena una nueva puntuación es similar al anterior. La única diferencia está en las clases que parametriza el AsyncTask. Ahora, como entrada, hay que introducir tres strings: puntos, nombre y fecha de la puntuación. Además, la tarea no nos devuelve ninguna información.

5.    Busca en la clase las apariciones de Log.e(…); y añade en la fila inferior cancel(true);. En total tienes que añadir 5 líneas. Con esto indicamos que queremos que se cancele la tarea en caso de error.

6.   Modifica la clase MainActivity.java y las res/values/arrays.xml para que el nuevo tipo de almacenamiento pueda ser seleccionado.

7.   Modifica el código correspondiente para que la nueva clase pueda ser seleccionada como almacén de las puntuaciones.

8.   Verifica el funcionamiento.

9.   Para verificar que su comportamiento es robusto ante errores en la red, desconecta el acceso a Internet del dispositivo y verifica que al listar las puntuaciones te indica: “Error al conectar con servidor”. Prueba a introducir la llamada sleep(5) en el fichero lista.php del servidor. Con esto se añade un retardo de 5 segundos en la respuesta. Verifica que al listar las puntuaciones te indica: “Tiempo excedido al conectar”.