En el desarrollo Android, las operaciones multihilo son esenciales para evitar el bloqueo de la interfaz de usuario (UI). Este artículo demuestra cómo utilizar objetos Handler, colas de mensajes y AsyncTask para gestionar tareas en segundo plano, con eejmplos prácticos que incluyen layouts XML y código Java modificado.
Ejemplo 1: Uso básico de Handler para actualizar la UI desde un hilo secundario
Se define un layout con botones para controlar un contaodr que se actualiza en la UI. El código Java emplea un Handler para enviar mensajes desde un hilo de trabajo al hilo principal.
Layout: activity_principal.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contenedor_principal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/vista_contador"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="0"
android:textSize="32sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="iniciarConteo"
android:text="Iniciar" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="detenerConteo"
android:text="Detener" />
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="comunicacionEntreHilos"
android:text="Comunicación entre hilos" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="ejecutarTareaAsincrona"
android:text="Tarea asíncrona" />
</LinearLayout>
Código Java: ActividadPrincipal.java
package com.ejemplo.multihilo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class ActividadPrincipal extends Activity {
private TextView vistaContador;
private int valorActual = 0;
private boolean enEjecucion = false;
private Handler manejadorPrincipal = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == 100) {
vistaContador.setText(String.valueOf(msg.arg1));
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
vistaContador = findViewById(R.id.vista_contador);
Toast.makeText(this, "Aplicación iniciada", Toast.LENGTH_SHORT).show();
new Thread(() -> {
SystemClock.sleep(5000);
runOnUiThread(() -> Toast.makeText(this, "Tarea de fondo completada", Toast.LENGTH_SHORT).show());
}).start();
}
public void iniciarConteo(View vista) {
enEjecucion = true;
new Thread(() -> {
while (enEjecucion) {
valorActual++;
Message mensaje = manejadorPrincipal.obtainMessage(100, valorActual, 0);
manejadorPrincipal.sendMessage(mensaje);
SystemClock.sleep(1000);
}
}).start();
}
public void detenerConteo(View vista) {
enEjecucion = false;
valorActual = 0;
}
public void comunicacionEntreHilos(View vista) {
startActivity(new Intent(this, ActividadComunicacion.class));
}
public void ejecutarTareaAsincrona(View vista) {
startActivity(new Intent(this, ActividadDescarga.class));
}
}
Ejemplo 2: Comunicación directa entre hilos secundarios
En este escenario, un hilo genera números aleatorios y los envía a otro hilo para procesamiento, usando Handlers y Loopers. El resultado se muestra en la UI.
Layout: activity_comunicacion.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contenedor_comunicacion"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/vista_resultado"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Esperando..."
android:textSize="28sp" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="iniciarProceso"
android:text="Iniciar proceso" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="detenerProceso"
android:text="Detener proceso" />
</LinearLayout>
Código Java: ActividadComunicacion.java
package com.ejemplo.multihilo;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import java.util.Random;
public class ActividadComunicacion extends Activity {
private TextView vistaResultado;
private Handler manejadorUI;
private Handler manejadorProcesador;
private boolean activo = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_comunicacion);
vistaResultado = findViewById(R.id.vista_resultado);
manejadorUI = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == 200) {
vistaResultado.setText("Resultado: " + msg.arg1);
}
}
};
new Thread(() -> {
Looper.prepare();
manejadorProcesador = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 100) {
int entrada = msg.arg1;
int resultado = entrada * entrada;
Log.d("Procesador", "Entrada: " + entrada + ", Resultado: " + resultado);
Message respuesta = manejadorUI.obtainMessage(200, resultado, 0);
manejadorUI.sendMessage(respuesta);
}
}
};
Looper.loop();
}).start();
}
public void iniciarProceso(View vista) {
activo = true;
new Thread(() -> {
Random generador = new Random();
while (activo) {
int aleatorio = generador.nextInt(50);
Log.d("Generador", "Número generado: " + aleatorio);
Message msg = manejadorProcesador.obtainMessage(100, aleatorio, 0);
manejadorProcesador.sendMessage(msg);
SystemClock.sleep(800);
}
}).start();
}
public void detenerProceso(View vista) {
activo = false;
}
}
Ejemplo 3: Uso de AsyncTask para simular una descarga con progreso
AsyncTask simplifica la ejecución de tareas largas en segundo plano con actualizaciones de UI. Se muestra una barra de progreso y mensajes de estado.
Layout: activity_descarga.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contenedor_descarga"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<ProgressBar
android:id="@+id/barra_progreso"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="iniciarDescarga"
android:text="Iniciar descarga" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="cancelarDescarga"
android:text="Cancelar descarga" />
</LinearLayout>
Código Java: ActividadDescarga.java
package com.ejemplo.multihilo;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
public class ActividadDescarga extends Activity {
private ProgressBar barraProgreso;
private TareaDescarga tareaActual;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_descarga);
barraProgreso = findViewById(R.id.barra_progreso);
}
public void iniciarDescarga(View vista) {
tareaActual = new TareaDescarga();
tareaActual.execute();
}
public void cancelarDescarga(View vista) {
if (tareaActual != null) {
tareaActual.cancel(true);
}
}
private class TareaDescarga extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onPreExecute() {
Toast.makeText(ActividadDescarga.this, "Descarga iniciada", Toast.LENGTH_SHORT).show();
barraProgreso.setProgress(0);
}
@Override
protected Boolean doInBackground(Void... params) {
for (int progreso = 1; progreso <= 100; progreso++) {
if (isCancelled()) return false;
publishProgress(progreso);
SystemClock.sleep(50);
}
return true;
}
@Override
protected void onProgressUpdate(Integer... valores) {
barraProgreso.setProgress(valores[0]);
}
@Override
protected void onPostExecute(Boolean exito) {
String mensaje = exito ? "Descarga completada" : "Descarga fallida";
Toast.makeText(ActividadDescarga.this, mensaje, Toast.LENGTH_SHORT).show();
}
}
}
Estos ejemplos ilustran patrones comunes para manejar la concurrencia en Android, asegurnado que la UI permanezca receptiva mientras se ejecutan operaciones costosas en segundo plano.