Cómo manejar tareas en segundo plano con AsyncTask y Android

Para que una aplicación sea ágil y dinámica, en ocasiones debemos realizar tareas en segundo plano, como puede ser la descarga de información de internet mientras al usuario se le permite seguir realizando otras tareas. En ocasiones, también debemos realizar procesos que nos piden si o si que lo hagamos en un hilo secundario al principal. Para todo esto nos podemos ayudar de la clase AsyncTask.

Esta clase nos va a permitir realizar tareas en segundo plano sin necesidad de utilizar directamente ni Handlers ni Threads, y digo directamente porque lo que va a hacer es tratar estos elementos de forma totalmente transparente al programador.

Cuando definimos una clase AsyncTask debemos definir el tipo de tres elementos, los parámetros de entrada, su progreso y el resultado. Y en caso de que alguno no se tenga que definir se le pasa Void omo argumento. Veamos un ejemplo de como heredar esta clase:

public class MyAsyncTask extends AsyncTask<String, Integer, Long> {}

Lo que se está haciendo aquí es decirle que los parámetros de entrada serán de tipo String, para el progreso se utilizará un Integer y el resultado será de tipo Long. Evidentemente aquí cada uno va a poder personalizarlo según sus necesidades.

Ahora vamos a ver como sería su ciclo de vida. Una vez que se llama al execute de la clase vamos a tener 4 pasos, más uno opcional:

  1. onPreExecute, se va a ejecutar antes de enviar a nuestro hilo secundario todo el proceso y lo podemos utilizar para inicializar tareas como mostrar una barra de progreso.
  2. doInBackground(Params...), aquí programamos nuestra tarea en segundo plano. Params en el caso que expusimos antes va a ser un array de Strings que se pasan cuando se llama al método execute. Al ser un proceso que puede durar un tiempo indeterminado en completarse, podemos darle un feedback al usuario del porcentaje completado gracias al método publishProgress(Progress...) que ya viene incluido en la clase AsyncTask. Este recibe un parámetro Integer como se indico en la definición de la clase y hará que se ejecute el método onProgressUpdate.
  3. onProgressUpdate(Progress...), como ya expliqué recogerá el progreso de la operación. En este caso el parámetro que tendremos será un Integer.
  4. onPostExecute(Result), una vez finalizado el proceso en segundo plano se ejecuta este método y recibe el resultado de toda la operación, en este caso será un tipo Long.
  5. onCancelled, como imaginareis se lanza cuando la operación en segundo plano se cancela. Para cancelar la operación basta con llamar al método cancel de AsyncTask. Y en cualquier momento se puede comprobar si la operación está cancelada con isCancelled que nos devuelve un boolean.
Fijaros que he utilizado diferentes tipos de datos únicamente para que sea más fácil diferenciar cada unos de ellos, pero se podría utilizar el mismo tipo de datos en los 3 casos. Vamos a ver un ejemplo de una clase de AsyncTask, en este caso le llamaremos AsyncDownloadTask.

public class AsyncDownloadTask extends AsyncTask<URL, Integer, JSONObject> {

 public interface AsyncTaskListener{ void onInit(); void onProgressUpdate(Integer progress); void onCancel(); void onFinish(JSONObject json);}
 private AsyncTaskListener asyncTaskListener;
 
 
 public AsyncDownloadTask(AsyncTaskListener asyncTaskListener){
  this.asyncTaskListener = asyncTaskListener;
 }
 
 @Override
 protected JSONObject doInBackground(URL... urls) {
  
  int  count = urls.length;
  JSONObject json = new JSONObject();
  
  
  for(int i = 0; i < count; i++){
   if(isCancelled())
    break;
   
   HttpClient httpclient = new DefaultHttpClient();

      HttpGet httpget = new HttpGet(urls[i].toString());

     
      HttpResponse response;
      try {
          response = httpclient.execute(httpget);
          
          if(response.getStatusLine().getStatusCode() == 200){
          
           HttpEntity entity = response.getEntity();
          
           if (entity != null) { 
               InputStream instream = entity.getContent();
               String result= convertStreamToString(instream);
               json.put("response_" + String.valueOf(i+1), new JSONObject(result));
               
               instream.close();
           }

    } else {
     //cancel(true);
    }
      } catch (Exception e) {
       cancel(true);
      }
   
   publishProgress((int) ((i / (float) count) * 100 ));
   
  }
  
  return json;
 }
 
 private static String convertStreamToString(InputStream is) {
     BufferedReader reader = new BufferedReader(new InputStreamReader(is));
     StringBuilder sb = new StringBuilder();

     String line = null;
     try {
         while ((line = reader.readLine()) != null) {
             sb.append(line + "\n");
         }
     } catch (IOException e) {
         e.printStackTrace();
     } finally {
         try {
             is.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
     return sb.toString();
 }
 
 @Override
 protected void onPreExecute(){
  asyncTaskListener.onInit();
 }
 
 @Override
 protected void onProgressUpdate(Integer... progress){
  asyncTaskListener.onProgressUpdate(progress[0]);
 }
 
 @Override
 protected void onPostExecute(JSONObject json){
  asyncTaskListener.onFinish(json);  
 }
 
 @Override
 protected void onCancelled(){
  asyncTaskListener.onCancel();
 }

}


Como veis tiene todos los elementos de los que hablamos antes: onPreExecute, doInBackground... En este ejemplo le he asignado a otros tipos, a los parámetros de entrada los he definido como URL, el progress será integer y el resultado será un objeto JSON. Fijaros como en el evento onPostExecute al definir que devolvemos un JSONObject deberá tener este como argumento.

Normalmente, en los ejemplos que he visto crean esta clase dentro de la clase de la Activity donde se va a utilizar, y por ejemplo desde el onPostExecute llaman a métodos de la Activity para realizar determinadas acciones, pero personalmente no me gusta utilizarla así. Para tenerla lo más genérica posible he creado un listener, AsyncTaskListener, y que se setea al inicializar la clase.

El método doInBackground en el caso que nos ocupa se va a encargar de realizar la descarga del contenido de las urls que se le pasan como argumento y a medida que las va descargando almacena el resultado en un objeto JSON.

El como descargamos el contenido de las urls aquí es secundario, simplemente nos servirá para generar un proceso asíncrono en segundo plano. Para que pueda funcionar correctamente debéis acordaros de incluir en el archivo AndroidManifest.xml los permisos correspondientes de internet:

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

Lo que si me gustaría remarcar del proceso doInBackground es que en el bucle for se está comprobando si se ha cancelado el proceso y además si ocurre alguna excepción se cancela el proceso. Además al final del bucle for se está llamando al método publishProgress pasándole el porcentaje de urls descargadas. Este llega a onProgressUpdate que lanza el evento del listener y aquí se podrá por ejemplo mostrar al usuario una barra de progreso.

Vamos a ver como utilizar está clase en nuestras Activities o donde lo necesitéis.

AsyncDownloadTask asyncdownload = null;
  
  URL url1 = null;
  URL url2 = null;
  URL url3 = null;
  URL url4 = null;
  
  try {
   url1 = new URL("url 1");
   url2 = new URL("url 2");
   url3 = new URL("url 3");
   url4 = new URL("url 4");
   
  } catch (MalformedURLException e) {}
  
  
  asyncdownload = new AsyncDownloadTask(new AsyncTaskListener() {
   
   @Override
   public void onProgressUpdate(Integer progress) {}
   
   @Override
   public void onInit() {}
   
   @Override
   public void onFinish(JSONObject json) {}
   
   @Override
   public void onCancel() {}
  });
  
  asyncdownload.execute(url1, url2, url3, url4);


Muy simple, tenemos nuestra clase, la cual instanciamos pasándole una instancia nueva de nuestro listener, en cada uno de sus eventos cada uno implementará sus propios procesos. Y creamos los argumentos que le pasaremos en el método execute de nuestra clase. En este caso 4 URL.

Podemos modificar un poco este ejemplo y en vez de pasar un número determinado de argumentos, pasarle un array de URL, así:

try {
   URL[] urls = {new URL("url1"), new URL("url2"), new URL("url3"), new URL("url4")};   
   asyncdownload.execute(urls); 
  } catch (MalformedURLException e) {}

En este caso se inicializa también el array con 4 parámetros, pero ya sabéis que hay formas de añadir indeterminados parámetros a un array. Lo importante es que veáis las dos formas de pasar parámetros al método execute.

Comments are closed.