En el desarrollo de aplicaciones Android, la gestión adecuada de hilos es esencial para mantener la fluidez de la interfaz de usuario y el rendimiento general. Un hilo principal sobrecargado con tareas prolongadas puede causar bloqueos, pérdida de cuadros e incluso el temido error ANR (Application Not Responding). Para evitar esto, se han desarrollado diversas técnicas de programación asincrónica, desde mecanismos clásicos como AsyncTask hasta soluciones modernas con Kotlin Coroutines.
AsyncTask: Un Enfoque Legado
AsyncTask fue una herramienta popular para ejecutar tareas en segundo plano con actualizaciones en la interfaz. Su estructura incluye métodos de devolución de llamada como onPreExecute(), doInBackground(), onProgressUpdate() y onPostExecute(). Internamente, AsyncTask utiliza una combinación de un pool de hilos y un Handler para la comunicación entre hilos.
Por ejemplo, una tarea para cargar datos podría implementarse así:
public class CargaDatosTarea extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() {
barraProgreso.setVisibility(View.VISIBLE);
}
@Override
protected String doInBackground(String... parametros) {
for (int i = 0; i < parametros.length; i++) {
obtenerDatosDesdeRed(parametros[i]);
publicarProgreso((i + 1) * 100 / parametros.length);
if (isCancelled()) return "Cancelado";
}
return "Completado";
}
@Override
protected void onProgressUpdate(Integer... progreso) {
barraProgreso.setProgress(progreso[0]);
}
@Override
protected void onPostExecute(String resultado) {
barraProgreso.setVisibility(View.GONE);
mostrarNotificacion(resultado);
}
}
AsyncTask gestiona una cola de tareas mediante un ThreadPoolExecutor, pero por defecto ejecuta las tareas de forma secuencial. Aunque permite la ejecución paralela, esto introduce complejidad en el control de concurrencia. Además, AsyncTask presenta probelmas conocidos como fugas de memoria cuando se declara como una clase interna no estática, ya que retiene referencias a la actividad. También carece de conciencia del ciclo de vida, lo que puede llevar a excepciones al intentar actualizar una vista destruida. Debido a estas limitaciones, AsyncTask ha sido descontinuado en versiones recientes de Android.
Handler y Looper: El Mecanismo Subyacente
El sistema Handler/Looper es la base para la comunicación entre hilos en Android. Sigue un modelo de productor-consumidor, donde un Handler envía mensajes a una MessageQueue, y un Looper los procesa en el hilo correspondiente. Cada hilo puede tener un solo Looper, gestionado mediante ThreadLocal para garantizar la exclusividad.
Por ejemplo, para enviar un mensaje de actualización desde un hilo secundario al principal:
Handler manejadorPrincipal = new Handler(Looper.getMainLooper());
new Thread(() -> {
// Realizar trabajo en segundo plano
String datos = cargarDatos();
manejadorPrincipal.post(() -> {
textView.setText(datos);
});
}).start();
El método Looper.loop() ejecuta un bucle infinito que bloquea eficientemente a la espera de mensajes, sin consumir CPU cuando la cola está vacia, gracias a mecanismos como epoll en Linux. MessageQueue organiza los mensajes por tiempo de ejecución, permitiendo tareas diferidas. Una práctica recomendada es reutilizar objetos Message con Message.obtain() para reducir la presión en el recoelctor de basura. Para evitar fugas, se deben usar Handler estáticos con referencias débiles a la vista o actividad.
Kotlin Coroutines: La Solución Moderna
Kotlin Coroutines ofrece un enfoque más limpio y seguro para la programación asincrónica. Las funciones de suspensión, marcadas con suspend, permiten pausar la ejecución sin blqouear el hilo, facilitando código secuencial para tareas asincrónicas. Los constructores como launch, async y withContext simplifican la gestión de hilos y la obtención de resultados.
Integrado con ViewModel y LiveData, se logra una gestión de ciclo de vida automática. Por ejemplo:
class MiViewModel : ViewModel() {
private val repositorio = RepositorioUsuario()
val datosUsuario: LiveData<ResultadoUsuario> = liveData {
emit(ResultadoUsuario.Cargando)
try {
val usuario = withContext(Dispatchers.IO) { repositorio.obtenerUsuario() }
emit(ResultadoUsuario.Exito(usuario))
} catch (e: Exception) {
emit(ResultadoUsuario.Error(e.message))
}
}
}
Al observar datosUsuario en una actividad, la UI se actualiza de forma reactiva. Las corrutinas se cancelan automáticamente cuando el ViewModel se destruye, eliminando riesgos de fugas de memoria. Esto permite un código más mantenible y robusto en comparación con métodos anteriores.