Android

Android


14.HILOS Y CONTROLADORES

Página 16 de 25

14.   HILOS Y CONTROLADORES

14.1.   Ejecuciones en background con Thread

En Java, y también en Android, podemos ejecutar varias secciones de un programa en paralelo. Cada uno de estos procesos se denomina thread o hilo, y se ejecuta en background con una prioridad determinada bajo el control de un objeto de la clase Thread de Java. Generalmente sólo hay un hilo asociado a una actividad, el hilo principal, consistente en una serie de instrucciones o sentencias que se ejecutan secuencialmente.

Una forma de crear un hilo es definir una clase que extiende a la clase Thread. La nueva clase debe sobrescribir el método run(), que se ejecutará cuando se inicie el hilo:

class Hilo extends Thread{

// ...

@Override

public void run(){

// instrucciones a ejecutar

}

// ...

}

Para comenzar la ejecución del hilo se instancia un objeto de la clase Hilo y se ejecuta el método start():

Hilo hilo1 = new Hilo();

hilo1.start()

Podemos definir varios hilos y ejecutarlos todos al mismo tiempo con distintas prioridades.

La vista de la pantalla sólo se puede modificar desde el hilo principal. Los procesos que se ejecutan en un hilo no tienen permitido modificar los objetos de tipo View, por ejemplo un TextView. Si lo intentamos, el programa se compilará, pero dará un error durante la ejecución. Si queremos mostrar en pantalla los datos o gráficos generados en un hilo podemos utilizar un controlador (handler), que es un objeto de la clase Handler. Un controlador residente en el hilo principal puede recibir mensajes de otro hilo y ejecutar instrucciones en consecuencia, por ejemplo, escribir en pantalla.

Para enviar un mensaje a un controlador handler desde un hilo:

•   Se obtiene el mensaje asociado al controlador

        Message msg=handler.obtainMessage();

•   Se construye un objeto de tipo Bundle para empaquetar los datos

        Bundle b =new Bundle();

•   Insertamos los datos en el bundle mediante parejas ("etiqueta", dato), por ejemplo para insertar un entero y una cadena en el bundle

        b.putInt("etiqueta1",entero);

        b.putString(``etiqueta2", cadena);

•   Finalmente se inserta el bundle en el mensaje y se envia al controlador

        msg.setData(b);

        handler.sendMessage(msg);

En cuanto se envia un mensaje al controlador, el sistema ejecuta el método  handler.handleMessage() de la clase Handler, que se habrá reescrito para aceptar el mensaje enviado y ejecutar otras instrucciones. Hay que definir el controlador handler en el hilo principal:

Handler handler= new Controlador();

donde la clase Controlador extiende la clase Handler y sobreescribe (Override) el método handleMessage():

class Controlador extends Handler{

       @Override

       public void handleMessage(Message msg){

         int entero=msg.getData().getInt("etiqueta1");

         String cadena=msg.getData().getString("etiqueta2");

            // otras instrucciones....

         }

}

Para leer el contenido del mensaje se usa el método msg.getData(), que extrae el objeto bundle, de donde podemos leer los datos usando getInt("etiqueta1") y getString("etiqueta2"). A continuación, podemos ejecutar otras instrucciones que se realizarán en el hilo principal.

Todo esto se pone en claro en el siguiente ejemplo. Ejecutamos dos hilos simultáneos, que envían mensajes a un controlador, que los muestra en pantalla. Cada hilo cuenta del 1 al 10 con un retraso temporal expresado en milisegundos. Para este ejemplo utilizamos la siguiente interfaz de usuario definida en el fichero main.xml.

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

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

      android:background="#ffffbb"

      android:orientation="vertical"

      android:layout_width="fill_parent"

      android:layout_height="fill_parent"

      >

  <TextView

      android:textColor="#550000"

      android:textSize="25sp"

      android:layout_width="fill_parent"

      android:layout_height="wrap_content"

      android:text="@string/hello"

      android:id="@+id/texto1"

      />

</LinearLayout

La actividad Hilos.java es la siguiente:

package es.ugr.amaro;

import android.app.Activity;

import android.os.Bundle;

import android.os.Handler;

import android.os.Message;

import android.widget.TextView;

public class Hilos extends Activity {

      Handler handler=new Controlador();

      TextView texto1,texto2;

      @Override

      public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

          setContentView(R.layout.main);

          texto1=(TextView) findViewById(R.id.texto1);

          Hilo hilo1=new Hilo(10,100);

          Hilo hilo2=new Hilo(5,200);

          hilo2.setPriority(7);

          hilo1.start();

          hilo2.start();

      }

      class Hilo extends Thread{

        int maximo,tiempo;

        Hilo(int n,int t){

           maximo=n;

           tiempo=t;

        }

        @Override

        public void run(){

           for (int i=0;i<=maximo;i++){

           try{

              Thread.sleep(tiempo);

              }

           catch (InterruptedException e){ ; }

           // Construye el mensaje

           // para enviar al controlador handler

           Message msg=handler.obtainMessage();

           // inserta datos en el mensaje

           //enpaquetandolos en un bundle

           Bundle b =new Bundle();

           b.putInt("total",i);

           b.putString("thread", currentThread().toString());

           msg.setData(b);

           // envia el mensaje

           handler.sendMessage(msg);

           }

        }

      }

 // Controlador para recibir mensajes del hilo

     class Controlador extends Handler{

       @Override

       public void handleMessage(Message msg){

          int total;

          // recibe los datos enviados en el mensaje msg

          total=msg.getData().getInt("total");

          String thread=msg.getData().getString("thread");

          texto1.append("\n"+total+"   "+thread);

       }

     }

}

Figura 14.1. Dos hilos cuentan del 1 al 10 y un controlador

va mostrando el resultado en la pantalla.

El resultado se ve en la figura 14.1. En la primera columna escribimos el entero i, a medida que el controlador lo va recibiendo y, en la segunda, el hilo del que proviene. Hemos usado el método currentThread().toString() que devuelve información sobre el hilo actual y lo almacena en una cadena. La prioridad del segundo hilo la hemos modificado con hilo2.setPriority(7). La prioridad por defecto de un hilo es 5. El valor de i se va incrementando en cada hilo lentamente debido al retraso, expresado en microsegundos mediante la instrucción Thread.sleep(tiempo).

14.2.   Diálogos de progreso

La técnica anterior se puede aplicar para mostrar una barra o diálogo de progreso, indicando que se está ejecutando un proceso en background. La barra de progreso es un tipo de diálogo: un objeto de la clase ProgressDialog, que extiende a Dialog, y es una actividad flotante. El tipo de diálogo se indica mediante setProgressStyle(estilo), donde estilo puede ser 0 (spinner) ó 1 (horizontal). Se inicia mediante showDialog(int id), donde estilo puede ser 0 (spinner) ó 1 (horizontal). Esto hace que se ejecuten los métodos onCreateDialog() y onPrepareDialog() de la clase Activity, que deben ser redefinidos (@Override) en nuestra actividad para que la barra de progreso se muestre de acuerdo con nuestras necesidades. El método onCreateDialog() sólo se ejecuta la primera vez que mostramos el diálogo. Si lo mostramos sucesivas veces, sólo se ejecutará el método onPrepareDialog().

En el ejemplo que viene a continuación se abre una barra de progreso de tipo horizontal. Utilizaremos la interfaz de usuario definida en el siguiente fichero  main.xml:

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

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

       android:background="#ffffff"

       android:orientation="vertical"

       android:layout_width="fill_parent"

       android:layout_height="fill_parent"

       >

<TextView

       android:textColor="#000000"

       android:layout_width="fill_parent"

       android:layout_height="wrap_content"

       android:text="@string/hello"

       android:id="@+id/texto"

       />

<Button android:text="Empezar"

       android:id="@+id/button1"

       android:layout_width="fill_parent"

       android:layout_height="wrap_content">

</Button>

</LinearLayout>

Nuestra actividad viene en el siguiente programa BarraDeProgreso.java:

package es.ugr.amaro;

import android.app.Activity;

import android.app.Dialog;

import android.app.ProgressDialog;

import android.os.Bundle;

import android.os.Handler;

import android.os.Message;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

import android.widget.TextView;

public class BarraDeProgreso extends Activity implements

OnClickListener{

  ProgressDialog progreso;

  Controlador handler=new Controlador();

  int maximo=100;

  int delay=100;

  int estilo=1;

  TextView texto;

        /** Called when the activity is first created. */

        @Override

        public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);

            setContentView(R.layout.main);

            Button boton= (Button) findViewById(R.id.button1);

            boton.setOnClickListener(this);

            texto=(TextView) findViewById(R.id.texto);

        }

  @Override

  public void onClick(View v) {

      showDialog(0);

  }

    // Crea una barra de progeso

    @Override

    public Dialog onCreateDialog(int id) {

            progreso = new ProgressDialog(this);

            progreso.setProgressStyle(estilo);

            progreso.setMessage("");

            return progreso;

    }

    // Prepara la barra de progeso

    @Override

    public void onPrepareDialog(int id,Dialog dialog) {

             progreso=(ProgressDialog)dialog;

            progreso.setProgressStyle(estilo);

            progreso.setMax(maximo);

            progreso.setProgress(0);

            progreso.setMessage(

                       "Ejecutando hilo en background...");

            Hilo thread=new Hilo();

            thread.start();        

    }

    class Controlador extends Handler{

      @Override

      public void handleMessage(Message msg){

            int total=msg.getData().getInt("total");

            progreso.setProgress(total);

            texto.setText("Total "+total+" Maximo: "+maximo);

            if(total==maximo){

               removeDialog(0);

            }

      }

    }

    class Hilo extends Thread{

      @Override

      public void run(){

            for (int i=0;i<=maximo;i++){

            try{

               Thread.sleep(delay);

               } catch (InterruptedException e){;}

            Message msg=handler.obtainMessage();

            Bundle b =new Bundle();

            b.putInt("total",i);

            msg.setData(b);

            handler.sendMessage(msg);

            }

      }

  }

}

Vemos el resultado en la figura 14.2 (izquierda). Pulsando el botón se inicia el diálogo, ejecutándose los métodos onCreateDialog() y onPrepareDialog(). En éste se inicia el proceso thread de la clase Hilo, que consiste en un ciclo que cuenta del uno al cien con un retraso de 100 milisegundos. Se envía un mensaje al controlador con la variable incremental del ciclo. En cada paso del ciclo el controlador actualiza el valor mostrado en la barra de progreso mediante progreso.setProgress(total). Cuando termina el ciclo (total=maximo), el controlador finaliza el diálogo con removeDialog(0). Entonces se devuelve el control al hilo principal y se puede repetir el proceso pulsando de nuevo el botón, en cuyo caso se ejecuta solamente onPrepareDialog().

Si cambiamos en este programa la variable estilo=0, y comentamos la instrucción progreso.setMax(maximo) tendremos un diálogo de progreso circular adecuado al caso en que no podamos predecir cuándo finalizará el hilo secundario. El resultado se ve en la figura 14.2 (derecha).

Figura 14.2. Diálogos de Progreso Horizontal y Giratorio.

14.3.   Interfaz Runnable

Existe otro procedimiento alternativo para crear un hilo. Consiste en definir una clase que implemente la interfaz Runnable. Esto requiere definir el método run(), que se ejecutará al iniciarse el hilo mediante:

Thread hilo= new Thread(runnable};

hilo.start();

donde runnable es un objeto de la clase que implementa la interfaz Runnable. La ventaja de este procedimiento en lugar de extender la clase Thread, como hemos hecho en este capítulo, es que cualquier clase puede implementar un interfaz. En particular puede ser una clase que a la vez extienda a la clase View y que podemos insertar en un layout. Así podemos tener varios objetos de tipo View en la pantalla, cada uno asociado a instrucciones que se están ejecutando en su propio hilo. Otra ventaja es que no es necesario utilizar un controlador, pues el contenido de estos objetos View puede ser actualizada continuamente usando el método postInvalidate() de la clase View. Este método hace que se vuelva a dibujar el contenido de un objeto View, ejecutándose el método onDraw()(en este caso no se puede usar invalidate() para dibujar de nuevo, porque este método sólo se puede utilizar en el hilo principal y no en hilos secundarios.)

En el siguiente ejemplo usamos esta técnica para actualizar dos contadores en dos hilos independientes. Cada hilo consiste en un objeto de la clase TextoAnimado que extiende a View y que hemos definido como clase interna. Cada hilo tiene un contador cuyo valor se incrementa con un retraso y se muestra en pantalla.

Estos objetos View se añadirán al layout definido en el siguiente fichero main.xml:

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

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

       android:background="#ffffff"

       android:orientation="vertical"

       android:layout_width="fill_parent"

       android:layout_height="fill_parent"

       android:id="@+id/layout"

       >

<TextView

android:id="@+id/texto"

       android:textColor="#000000"

       android:textSize="35sp"

       android:layout_width="wrap_content"

       android:layout_height="wrap_content"

       android:text="Interfaz Runnable"

       />

</LinearLayout>

El programa Java, donde definimos la actividad RunnableEjemplo, es el siguiente. Los dos contadores son los elementos de un array. Los parámetros de los objetos TextoAnimado son, además de la clase actual this, el retraso temporal y el índice del contador (0,1).

package es.ugr.amaro.runnableejemplo;

import android.app.Activity;

import android.content.Context;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.os.Bundle;

import android.view.View;

import android.view.ViewGroup.LayoutParams;

import android.widget.LinearLayout;

public class RunnableEjemplo extends Activity {

  boolean continuar=true;

  String mensaje="";

  int[] contador={0,0};

  TextoAnimado texto,texto2;

@Override

public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        LinearLayout ll=(LinearLayout)

        findViewById(R.id.layout);

        LayoutParams params=new LayoutParams(600,100);

        texto= new TextoAnimado(this,40,0);

        texto.setLayoutParams(params);

        ll.addView(texto);

        texto2= new TextoAnimado(this,200,1);

        texto2.setLayoutParams(params);

        texto2.setBackgroundColor(Color.YELLOW);

        ll.addView(texto2);

        Thread hilo= new Thread(texto);

        hilo.start();

        Thread hilo2= new Thread(texto2);

        hilo2.start();

}

class TextoAnimado extends View implements Runnable{

  int retraso;

  int i;

  Paint paint=new Paint();

  public TextoAnimado(Context context,int retraso,int i) {

        super(context);

        this.retraso=retraso;

        this.i=i;

        paint.setColor(Color.BLACK);

        paint.setTextSize(30);

        paint.setAntiAlias(true);

  }

  @Override

  protected void onDraw(Canvas canvas){

        super.onDraw(canvas);

        canvas.drawText(mensaje,50,50,paint);

  }

  public void run() {

        while(continuar){

           try{Thread.sleep(retraso);}

           catch(InterruptedException e){;}

           contador[i]++;

           mensaje="Contador: "+contador[i];

           postInvalidate();

        }

  }

}

}

Figura 14.3. Dos hilos en dos objetos View implementando la inerfaz Runnable.

El resultado se ve en la figura 14.3. Los dos contadores correspondientes a los índices i=0, 1 del array contador[], avanzan de uno en uno con un retardo de 40 y 200 milisegundos, respectivamente.

Si se ejecuta esta aplicación y se observa detenidamente, se verá que ocasionalmente alguno de los contadores (con más frecuencia el segundo) muestra el resultado correspondiente al otro contador, pero inmediatamente después continúa con su propio contaje. Esto se debe a que el texto a escribir, almacenado en la variable de la clase principal mensaje está siendo compartido por los dos hilos, que están ejecutándose simultaneamente. Entonces puede ocurrir que inmediatamente antes de que en un hilo se escriba la variable mensaje, el otro hilo modifique súbitamente su contenido.  Este ejemplo ilustra el tipo de interacciones que pueden producirse al trabajar con varios hilos simultáneamente. La solución en este caso es trasladar la declaración de la variable:

String mensaje="";

a la clase TextoAnimado, en cuyo caso cada hilo modificaría su propia copia de la variable mensaje.

14.4.   Notificaciones

La barra de estado, que aparece en la parte superior de la pantalla, puede mostrar las notificaciones que envían las aplicaciones que se ejecutan en background. Esto permite lanzar a ejecutar un hilo desde una actividad, pasar a otra actividad y comprobar si sigue ejecutándose. La notificación que envía el hilo consiste en un mensaje de texto que se puede examinar en la vista expandida de notificaciones, que se despliega tirando hacia abajo de la barra de estado.

Ilustraremos el uso de notificaciones creando una actividad que usa la siguiente interfaz de usuario en main.xml:

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

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

       android:orientation="vertical"

       android:layout_width="fill_parent"

       android:layout_height="fill_parent"

       android:background="#ffffff"

       >

<TextView

       android:layout_width="fill_parent"

       android:layout_height="wrap_content"

       android:text="Ejemplo de notificaciones desde un hilo"

       android:textSize="20sp"

       android:textColor="#000000"

       />

  <ImageView android:layout_width="wrap_content"

       android:src="@drawable/icon"

       android:id="@+id/imageView1"

       android:layout_height="wrap_content">

</ImageView>

<TextView

       android:layout_width="fill_parent"

       android:layout_height="wrap_content"  

       android:text="Obsérvese este icono a la derecha de la

       barra de estado"

       android:textSize="20sp"

       android:textColor="#000000"

       />

</LinearLayout>

Figura 14.4. Notificaciones desde un hilo ejecutándose en background. Izquierda:

Obsérvese el icono de notificación en la barra de estado. Derecha: Notificación

detallada mostrando el mensaje enviado por el hilo.

El fichero Java de la actividad es el siguiente. El proceso de creación de la notificación requiere seguir varios pasos que están indicados con comentarios en el listado. En concreto:

•   Instanciar una referencia al manager de notificaciones, que es un objeto de la clase NotificationManager.

•   Crear la notificación, un objeto de la clase Notification, que incluye un icono, un texto a mostrar al inicio y una fecha que no tiene otra utilidad que para ordenar las distintas notificaciones del sistema cronológicamente.

•   Definir un Intent, para acceder a nuestra aplicación desde la vista extendida de notificaciones y un PendingIntent. El PendingIntent contiene la información del intent y concede permisos para ser utilizado desde otra actividad externa.

•   Actualiza el texto de la notificación expandida, actualiza la notificación con setLatestEventInfo() y la notifica al manager con notify().

En este ejemplo, la actualización y notificación se ha incluido en un hilo que incrementa un contador con un retraso. El texto de la notificación incluye el valor del contador.

Figura 14.5. La notificación persiste en la barra de estado cuando cerramos

la actividad pulsando la tecla back y volviendo a la pantalla home,

ya que el hilo sigue ejecutándose en background.

package es.ugr.amaro.notificaciones;

import android.app.Activity;

import android.app.Notification;

import android.app.NotificationManager;

import android.app.PendingIntent;

import android.content.Context;

import android.content.Intent;

import android.os.Bundle;

public class Notificaciones extends Activity {

  boolean continuar=true;

  int retraso=100;

  int contador=0;

  // variables para la notificacion

  NotificationManager manager;

  Notification notificacion;

  CharSequence notificacionTitle = "Hilo funcionando";

  CharSequence notificacionText = "";

  Context context;

  Intent notificacionIntent;

  PendingIntent pendingIntent;

  /** Called when the activity is first created. */

   @Override

   public void onCreate(Bundle savedInstanceState) {

         super.onCreate(savedInstanceState);

         setContentView(R.layout.main);

         // referencia al manager de notificaciones

    String servicio=Context.NOTIFICATION_SERVICE;

    manager=(NotificationManager)getSystemService(

                                                servicio);

    // crea la notificacion

    int icon=R.drawable.icon;

    String texto="ejecutando";

    long cuando=System.currentTimeMillis();

    notificacion=new Notification(icon,texto,cuando);

    // define el intent de la notificacion expandida

    context = getApplicationContext();

    notificacionIntent = new Intent(this,

                                 Notificaciones.class);

    pendingIntent = PendingIntent.getActivity(this,

                             0, notificacionIntent, 0);

    // inicia un nuevo hilo

         Contar contar=new Contar();

         Thread hilo = new Thread(contar);

         hilo.start();

   }

// clase para ejecutar un hilo que manda una notificación class Contar implements Runnable{

    public void run() {

         while(continuar){

            try { Thread.sleep( retraso ); }

            catch ( InterruptedException e ){ ; }

            contador++;

            //texto de la notificacion expandida

            notificacionText="contador="+contador;

            //actualiza la notificacion

            notificacion.setLatestEventInfo(context,

                                  notificacionTitle,

                                   notificacionText,

                                    pendingIntent);

            // comunica la notificacion al manager

            int ref=1;

            manager.notify(ref,notificacion);

         }

      }

    }

}

El resultado se ve en la figura 14.4. Al ejecutar nuestra aplicación aparece en la barra de estado el icono de Android icon.png, indicando que la notificación está activa. Si desplegamos la barra de estado, tirando de ella hacia abajo, veremos que se muestra el mensaje de la notificación. Si pulsamos la tecla back o Home y salimos de nuestra aplicación, figura 14.5. vemos que la barra de estado continúa mostrando el icono de la notificación. Esto nos confirma que el hilo sigue ejecutándose en background. Si abrimos la vista detallada de notificaciones y pulsamos en la notificación, se abre de nuevo nuestra actividad (es aquí donde el pendingIntent está siendo utilizado).

Puede haber más de una notificación proveniente de distintos hilos o del mismo. El número de referencia ref=1 en la notificación al manager permite controlar qué notificación se está actualizando. Por ejemplo, si al final del método run() añadimos la línea:

manager.notify(2,notificacion);

estamos notificando al manager una segunda notificación. Al ejecutar nuestra aplicación ahora se verán dos iconos en la barra de estado y dos notificaciones en la vista detallada, figura 14.6.

Figura 14.6. Dos notificaciones en la

barra de estado y en la vista detallada de notificaciones.

Ir a la siguiente página

Report Page