sábado, 27 de diciembre de 2014

Hilos y Tareas Asíncronas (Thread y AsyncTask) en Android



Todos los componentes de una aplicación Android, tanto las actividades, los servicios [sí, también los servicios], o los broadcast receivers se ejecutan en el mismo hilo de ejecución, el llamado hilo principal, main thread o GUI thread, que como éste último nombre indica también es el hilo donde se ejecutan todas las operaciones que gestionan la interfaz de usuario de la aplicación. Es por ello, que cualquier operación larga o costosa que realicemos en este hilo va a bloquear la ejecución del resto de componentes de la aplicación y por supuesto también la interfaz, produciendo al usuario un efecto evidente de lentitud, bloqueo, o mal funcionamiento en general, algo que deberíamos evitar a toda costa. Incluso puede ser peor, dado que Android monitoriza las operaciones realizadas en el hilo principal y detecta aquellas que superen los 5 segundos, en cuyo caso se muestra el famoso mensaje de "Application Not Responding" (ANR) y el usuario debe decidir entre forzar el cierre de la aplicación o esperar a que termine.


Obviamente, éstos son el tipo de errores que nadie quiere ver al utilizar su aplicación, y en este apartado y los siguientes vamos a ver varias formas de evitarlo utilizando procesos en segundo plano para ejecutar las operaciones de larga duración. En este primer apartado de la serie nos vamos a centrar en dos de las alternativas más directas a la hora de ejecutar tareas en segundo plano en Android:

• Crear nosotros mismos de forma explícita un nuevo hilo para ejecutar nuestra tarea.
• Utilizar la clase auxiliar AsyncTask proporcionada por Android.

Mi idea inicial para este capítulo era obviar la primera opción, ya que normalmente la segunda solución nos es más que suficiente, y además es mas sencilla y más limpia de implementar. Sin embargo, si no comentamos al menos de pasada la forma de crear "a mano" nuevos hilos y los problemas que surgen, quizá no se viera demasiado claro las ventajas que tiene utilizar las AsyncTask. Por tanto, finalmente voy a pasar muy rápidamente por la primera opción para después centrarnos un poco más en la segunda. Además, aprovechando el tema de la ejecución de tareas en segundo plano, vamos a ver también cómo utilizar un control (el ProgressBar) y un tipo de diálogo (el ProgressDialog) que no vimos en los primeros temas del curso dedicados a la interfaz de usuario.

Y para ir paso a paso, vamso a empezar por crear una aplicación de ejemplo en cuya actividad principal colocaremos un control ProgressBar (en mi caso llamado pbarProgreso) y un botón (btnSinHilos) que ejecute una tarea de larga duración. Para simular una operación de larga duración vamos a ayudarnos de un método auxiliar que lo único que haga sea esperar 1 segundo, mediante una llamada a Thread. sleep().


Haremos que nuestro botón ejecute este método 10 veces, de forma que nos quedará una ejecución de unos 10 segundos en total:


Como veis, aquí todavía no estamos utilizando nada especial, por lo que todo el código se ejecutará en el hilo principal de la aplicación. En cuanto a la utilización del control ProgressBar vemos que es muy sencilla y no requiere apenas configuración. En nuestro caso tan sólo hemos establecido el valor máximo que alcanzará (el valor en el que la barra de progreso estará rellena al máximo) mediante el método setMax(100), posteriormente la hemos inicializado al valor mínimo mediante una llamada a setProgress(0) de forma que inicialmente aparezca completamente vacía, y por último en cada iteración del bucle incrementamos su valor en 10 unidades llamando a incrementProgressBy(10), de tal forma que tras la décima iteración la barra llegue al valor máximo de 100 que establecimos antes. Finalmente mostramos un mensaje Toast para informar de la finalización de la tarea. Pues bien, ejecutemos la aplicación, pulsemos el botón, y veamos qué ocurre.

No era eso lo que esperábamos ¿verdad? Lo que ha ocurrido es que desde el momento que hemos pulsado el botón para ejecutar la tarea, hemos bloqueado completamente el resto de la aplicación, incluida la actualización de la interfaz de usuario, que ha debido esperar a que ésta termine mostrando directamente la barra de progreso completamente llena. En definitiva, no hemos sido capaces de ver el progreso de la tarea. Pero como decíamos, este efecto puede empeorar. Probemos ahora a pulsar el botón de la tarea y mientras ésta se ejecuta realicemos cualquier acción sobre la pantalla, un simple click sobre el fondo nos basta. Veamos qué ocurre ahora.

Vemos cómo al intentar hacer cualquier acción sobre la aplicación Android nos ha advertido con un mensaje de error que la aplicación no responde debido a que se está ejecutando una operación de larga duración en el hilo principal. El usuario debe elegir entre esperar a que termine de ejecutarla o forzar el cierre de la aplicación. Pues bien, estos son los efectos que vamos a intentar evitar. La opción más inmediata que nos proporciona Android, al igual que otras plataformas, es crear directamente hilos secundarios dentro de los cuales ejecutar nuestras operaciones costosas. Esto lo conseguimos en Android instanciando un objeto de la clase Thread. El constructor de la clase Thread recibe como parámetro un nuevo objeto Runnable que debemos construir implementando su método run(), dentro del cual vamos a realizar nuestra tarea de larga duración. Hecho esto, debemos llamar al método start() del objeto Threaddefinido para comenzar la ejecución de la tarea en segundo plano.


Hasta aquí todo sencillo y relativamente limpio. Los problemas aparecen cuando nos damos cuenta que desde este hilo secundario que hemos creado no podemos hacer referencia directa a componentes que se ejecuten en el hilo principal, entre ellos los controles que forman nuestra interfaz de usuario, es decir, que desde el método run() no podríamos ir actualizando directamente nuestra barra de progreso de la misma forma que lo hacíamos antes. Para solucionar esto, Android proporciona varias alternativas, entre ellas la utilización del método post() para actuar sobre cada control de la interfaz, o la llamada al método runOnUiThread() para "enviar" operaciones al hilo principal desde el hilo secundario [Nota: Sí, vale, sé que no he nombrado la opción de los Handler, pero no quería complicar más el tema por el momento].
Ambas opciones requieren como parámetro un nuevo objeto Runnable del que nuevamente habrá que implementar su método run() donde se actúe sobre los elementos de la interfaz. Por ver algún ejemplo, en nuestro caso hemos utilizado el método post() para actuar sobre el control ProgressBar, y el método runOnUiThread()para mostrar el mensaje toast.


Utilicemos este código dentro de un nuevo botón de nuestra aplicación de ejemplo y vamos a probarlo en el emulador.

Ahora sí podemos ver el progreso de nuestra tarea reflejado en la barra de progreso. La creación de un hilo secundario nos ha permitido mantener el hilo principal libre de forma que nuestra interfaz de usuario de actualiza sin problemas durante la ejecución de la tarea en segundo plano. Sin embargo miremos de nuevo el código anterior. Complicado de leer, ¿verdad? Y eso considerando que tan sólo estamos actualizando un control de nuestra interfaz. Si el número de controles fuera mayor, o necesitáramos una mayor interacción con la interfaz el código empezaría a ser inmanejable, difícil de leer y mantener, y por tanto también más propenso a errores. Pues bien, aquí es donde Android llega en nuestra ayuda y nos ofrece la clase AsyncTask, que nos va a permitir realizar esto mismo pero con la ventaja de no tener que utilizar artefactos del tipo runOnUiThread() y de una forma mucho más organizada y legible. La forma básica de utilizar la clase AsyncTask consiste en crear una nueva clase que extienda de ella y sobrescribir varios de sus métodos entre los que repartiremos la funcionalidad de nuestra tarea. Estos métodos son los siguientes:

• onPreExecute(). Se ejecutará antes del código principal de nuestra tarea. Se suele utilizar para preparar la ejecución de la tarea, inicializar la interfaz, etc.
doInBackground(). Contendrá el código principal de nuestra tarea.
onProgressUpdate(). Se ejecutará cada vez que llamemos al método publishProgress() desde el método doInBackground().
• onPostExecute(). Se ejecutará cuando finalice nuestra tarea, o dicho de otra forma, tras la finalización del método doInBackground().
• onCancelled(). Se ejecutará cuando se cancele la ejecución de la tarea antes de su finalización normal.


Estos métodos tienen una particularidad esencial para nuestros intereses. El método doInBackground() se ejecuta en un hilo secundario (por tanto no podremos interactuar con la interfaz), pero sin embargo todos los demás se ejecutan en el hilo principal, lo que quiere decir que dentro de ellos podremos hacer referencia direct a nuestros controles de usuario para actualizar la interfaz. Por su parte, dentro de doInBackground() tendremos la posibilidad de llamar
periódicamente al método publishProgress() para que automáticamente desde el método onProgressUpdate() se actualice la interfaz si es necesario. Al extender una nueva clase de AsyncTask indicaremos tres parámetros de tipo:

• El tipo de datos que recibiremos como entrada de la tarea en el método doInBackground().

• El tipo de datos con el que actualizaremos el progreso de la tarea, y que recibiremos como parámetro del método onProgressUpdate() y que a su vez tendremos que incluir como parámetro del método publishProgress().

• El tipo de datos que devolveremos como resultado de nuestra tarea, que será el tipo de retorno del método doInBackground() y el tipo del parámetro recibido en el método onPostExecute().

En nuestro caso de ejemplo, extenderemos de AsyncTask indicando los tipos Void, Integer y Boolean respectivamente, lo que se traducirá en que:

• doInBackground() no recibirá ningún parámetro de entrada (Void).
• publishProgress() y onProgressUpdate() recibirán como parámetros datos de tipo entero (Integer).
• doInBackground() devolverá como retorno un dato de tipo booleano y onPostExecute() también recibirá como parámetro un dato del dicho tipo (Boolean).

Dicho esto, cómo repartiremos la funcionalidad de nuestra tarea entre los distintos métodos. Pues sencillo, en onPreExecute() inicializaremos la barra de progreso estableciendo su valor máximo y poniéndola a cero para comenzar. En doInBackground() ejecutaremos nuestro bucle habitual llamando a publishProgress() tras cada iteración indicando el progreso actual. En onProgressUpdate() actualizaremos el estado de la barra de progreso con el valor recibido como parámetro. Y por último en onPostExecute() mostraremos el mensaje Toast de finalización de la tarea. Veamos el código completo:



Si observamos con detenimiento el código, la única novedad que hemos introducido es la posibilidad de cancelar la tarea en medio de su ejecución. Esto se realiza llamando al método cancel() de nuestra AsyncTask (para lo cual añadiremos un botón más a nuestra aplicación de ejemplo, además del nuevo que añadiremos para comenzar la tarea). Dentro de la ejecución de nuestra tarea en doInBackground() tendremos además que consultar periodicamente el resultado del método isCancelled() que nos dirá si el usuario ha cancelado la tarea (es decir, si se ha llamado al método cancel()), en cuyo caso deberemos de terminar la ejecución lo antes posible, en nuestro caso de ejemplo simplemente saldremos del bucle con la instrucción break. Además, tendremos en cuenta que en los casos que se cancela la tarea, tras el método doInBackground() no se llamará a onPostExecute() sino al método onCancelled(), dentro del cual podremos realizar cualquier acción para confirma la cancelación de la tarea. En nuestro caso mostraremos un mensaje Toast informando de ello.

Mucho mejor que las alternativas anteriores, verdad? Pero vamos a mostrar una opción más. Si queremos que el usuario pueda ver el progreso de nuestra tarea en segundo plano, pero no queremos que interactúe mientras tanto con la aplicación tenemos la opción de mostrar la barra de progreso dentro de un diálogo. Android nos proporciona directamente un componente de este tipo en forma de un tipo especial de diálogo llamado ProgressDialog.

Configurar un cuadro de diálogo de este tipo es muy sencillo. Para ello vamos a añadir un botón más a nuestra aplicación de ejemplo, donde inicializaremos el diálogo y lanzaremos la tarea en segundo plano. Para inicializar el diálogo comenzaremos por crear un nuevo objeto ProgressDialog pasándole como parámetro el contexto actual. Tras esto estableceremos su estilo: STYLE_HORIZONTAL para una barra de progreso tradicional, o STYLE_SPINNER para un indicador de progreso de tipo indeterminado.



ProgressDialog horizontal


ProgressDialog spinner

Lo siguiente será especificar el texto a mostrar en el diálogo, en nuestro caso el mensaje "Procesando…", y el valor máximo de nuestro progreso, que lo mantendremos en 100. Por último indicaremos si deseamos que el diálogo sea cancelable, es decir, que el usuario pueda cerrarlo pulsando el botón Atrás del teléfono.
Para nuestro ejemplo activaremos esta propiedad para ver cómo podemos cancelar también nuestra tarea en segundo plano cuando el usuario cierra el diálogo. Tras la configuración del diálogo lanzaremos la AsyncTask del ejemplo anterior, que tendremos que modificar ligeramente para adaptarla al nuevo diálogo. Veamos el código por ahora:


La AsyncTask será muy similar a la que ya implementamos. De hecho el método doInBackground() no sufrirá cambios.

En onProgressUpdate() la única diferencia será que actualizaremos el progreso llamando al método setProgress() del ProgressDialog en vez de la ProgressBar.

El código de onPreExecute() sí tendrá algún cambio más. Aprovecharemos este método para implementar el evento onCancel del diálogo, dentro del cual cancelaremos también la tarea en segundo plano llamando al método cancel(). Además, inicializaremos el progreso del diálogo a 0 y lo mostraremos al usuario mediante el método show().

Por último, en el método onPostExecute() además de mostrar el Toast de finalización, tendremos que cerrar previamente el diálogo llamando a su método dismiss().

Veamos el código completo de la AsyncTask modificada para usar el nuevo ProgressDialog.



Si ahora ejecutamos nuestro proyecto y pulsamos sobre el último botón incluido veremos cómo el diálogo aparece por encima de nuestra actividad mostrando el progreso de la tarea asíncrona. Tras finalizar, el diálogo desaparece y se muestra el mensaje toast de finalización. Si en cambio, se pulsa el botón Atrás del dispositivo antes de que la tarea termine el diálogo se cerrará y se mostrará el mensaje de cancelación.


Saludos compañeros, aprovechen la información.








No hay comentarios:

Publicar un comentario en la entrada