Tip Android #02: realizar tareas asíncronas con AsyncTask

  1. 17/01/2013 (Primera publicación)
  2. 28/07/2015: Ampliado, código mejorado
  3. 07/12/2015: Ejecuciones en paralelo

android tip

Las tareas “pesadas” como consultas a servicios web se deben ejecutar en segundo plano sin bloquear la interfaz de usuario, y actualizar ésta una vez hayamos obtenido los datos correspondientes. De hecho, desde Honeycomb el StrictMode de Android por defecto no permite que en el UIThread (el hilo principal de la aplicación y desde el que se permite modificar la interfaz gráfica), se realicen operaciones de networking.

Para lanzar un hilo que realice tareas en segundo plano se suelen utilizar dos estrategias: usar los Thread propios de la API de Java, o bien la clase AsyncTask de Android que constituye una solución más simple y elegante. También existen otras opciones más específicas, como usar IntentService (servicio asíncrono) o el Loader AsyncTaskLoader introducido en Honeycomb.

Para usar AsyncTask, crearemos una clase interna en la que queramos realizar la tarea (podría ser una Activity, un Adapter…lo imprescindible es que sea creada y ejecutada dentro del UIThread) que herede ella. Al ejecutarla, se invocarán secuencialmente los siguientes métodos que podemos sobreescribir si lo necesitamos:

  • onPreExecute:Tiene acceso al UI thread y nos permite, por ejemplo, mostrar un ProgressDialog.
  • doInBackground: es donde se realizará la tarea que queremos realizar en segundo plano. No tenemos acceso a la interfaz gráfica, pero se puede invocar publishProgress si queremos actualizar el status de un ProgressDialog.
  • onProgressUpdate: es llamado por publishProgress desde doInBackground para que podamos informar al usuario del progreso de la tarea
  • onPostExecute: se invoca una vez haya terminado doInBackground y se utiliza para actualizar la interfaz gráfica.

Veamos un ejemplo típico como la obtención de una imagen desde la web para mostrarla en un ImageView (para trabajar con descarga de imágenes recomiendo utilizar la librería Glide).

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" 
    android:gravity="center">

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        />

    <ProgressBar
        
        android:id="@+id/progresss"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:visibility="gone"/>

</LinearLayout>
package com.danielme.tipsandroid.asynctask;

import java.net.URL;

import com.danieme.tipsandroid.asynctask.R;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;

import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;

public class MainActivity extends Activity {
	ProgressBar progressBar;
	ImageView imageView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		progressBar = (ProgressBar) findViewById(R.id.progresss);
		imageView = (ImageView) findViewById(R.id.imageView1);
		// ejecutamos la tarea asícrona (podemos pasarle un array de cadenas)
		new BackgroundTask()
				.execute("https://danielmedotcom.files.wordpress.com/2013/01/android-tip.png");
	}

	private class BackgroundTask extends AsyncTask<String, Void, Bitmap> {

		@Override
		protected void onPreExecute() {
			// mostramos el círculo de progreso
			progressBar.setVisibility(View.VISIBLE);
		}

		@Override
		protected Bitmap doInBackground(String... urls) {
			Bitmap bmp = null;
			try {
				// se detiene la ejecución del hilo 3 segundos para que de
				// tiempo a ver el progress
				Thread.sleep(3000);
				URL url = new URL(urls[0]);
				bmp = BitmapFactory.decodeStream(url.openConnection()
						.getInputStream());
			} catch (Exception e) {
				Log.e(MainActivity.class.toString(), e.getMessage());
			}
			return bmp;
		}

		@Override
		protected void onPostExecute(Bitmap result) {
			// la tarea en segundo plano ya ha terminado. Ocultamos el progreso.
			progressBar.setVisibility(View.GONE);
			// si tenemos la imagen la mostramos
			if (result != null) {
				MainActivity.this.imageView.setImageBitmap(result);
				MainActivity.this.imageView.setVisibility(View.VISIBLE);
			}
			// si no, informamos del error
			else {
				Builder builder = new Builder(MainActivity.this);
				builder.setTitle(R.string.title);
				builder.setIcon(android.R.drawable.ic_dialog_info);
				builder.setMessage(R.string.error);
				builder.setNeutralButton(getString(R.string.ok), null);

				AlertDialog alertDialog = builder.create();
				alertDialog.show();
				alertDialog.setCancelable(false);
			}
		}
	}

}

Obsérvese que la AsyncTask requiere tres tipos genéricos (puede ser Void), su significado en orden es:

  1. El tipo de parámetro que recibe el método doInBackground. Se trata de un array, en el ejemplo de tipo String.
  2. El tipo de parámetro que recibe el método onProgressUpdate
  3. El tipo de parámetro que devuelve el método doInBackground y que recibe onPostExecute. En el ejemplo es la imagen obtenida desde la url.

Al igual que cualquier otra clase, una AsyncTask puede tener los atributos que sean necesarios y ser establecidos a través del constructor correspondiente.

Cancelación

Se puede solicitar la cancelación de la ejecución de la AsyncTask en cualquier momento invocando el método cancel(true). La ejecución de este método provocará que las llamadas a isCancelled devuelvan true, lo que podemos usar dentro de doInBackground para detener las operaciones, y que, si el método onPostExecute aún no se ha invocado, en su lugar se llame a onCancelled. Obsérvese que en ningún caso la llamada a cancel “mata” la ejecución de la tarea en segundo plano y destruye la AsyncTask.

Una vez lanzada, la tarea asíncrona se ejecuta completamente hasta finalizar aunque la Activity ya no se encuentre en pantalla o haya sido destruída.

Por tanto, si se quiere detener una AsyncTask en ejecución al salir de la Activity, habrá que sobreescribir el método onPause u onDestroy,según lo que se quiera hacer, de la Activity para ejecutar el cancel de la AsyncTask y seguir lo indicado anteriormente. Una alternativa mejor es utilizar AsyncTaskLoader ya que las tareas asíncronas implementadas con este mecanismo están asociadas al ciclo de vida de la Activity o Fragment.

El siguiente código solicita la cancelación de una AsyncTask al finalizarse la Activity (no olvidar gestionar la cancelación dentro de los métodos de la AsyncTask).

@Override
    protected void onDestroy() {    
    super.onDestroy();
    myAsyncTask.cancel(true);
}

Ejecuciones múltiples

Las ejecuciones de una misma AsyncTask se realizan de forma secuencial, esto es, si ya hay una ejecución en curso hasta que esta no finalice no se realizará la siguiente ejecución solicitada. Para poder realizar ejecuciones en paralelo debemos lanzarlas con el método executeOnExecutor siempre y cuando estemos utilizamos la API 11 (Honeycomb).

new BackgroundTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url);

El proyecto completo para Eclipse ADT se encuentra en Github. Para más información sobre cómo utilizar GitHub, consultar este artículo.

<< TIPS ANDROID

2 Responses to Tip Android #02: realizar tareas asíncronas con AsyncTask

  1. jackfido dice:

    Que tal, buenas noches desde México, el motivo de mi consulta basicamente es el siguiente, estoy creando una aplicacion android con UI la cual conecta a un WS de .NET (WCF mejor dicho), tengo 3 clases; la 1ra es mi login, la 2da es el asynctask y la 3era es el metodo de llamado al WCF, lo tengo separado de esta manera para reutilizar el codigo ya que seran varias pantallas, ahora, mi asynctask esta contruido “igual” al tuyo, la unica diferencia es nombres de objetos, pero al momento de clickear un boton para el login (que es el disparador de mi asynctask) no ejecuta el progressdialog :S me podrias apoyar con tus conocimientos, si gustas publico algo de codigo o no se como pueda expresarme mejor

    • danielme.com dice:

      El ProgressDialog debes mostrarlo justo antes de ejecutar la AsyncTask o bien dentro de la propia AsyncTask en el método onPreExecute. Depura y comprueba qué puede estar pasando.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: