Asynchronous Updates
La API Environment.runLater() proporciona un mecanismo para actualizar de manera segura la interfaz de usuario desde hilos en segundo plano en aplicaciones webforJ. Esta característica experimental permite operaciones asincrónicas mientras mantiene la seguridad de los hilos para las modificaciones de la interfaz de usuario.
The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:
- "Refresh this dashboard every 30 seconds."
- "Add a search-as-you-type debouncer."
- "Run this CPU-heavy work in the background and update the progress bar."
Comprendiendo el modelo de hilos
webforJ impone un estricto modelo de hilos donde todas las operaciones de la interfaz de usuario deben ocurrir en el hilo Environment. Esta restricción existe porque:
- Restricciones de la API de webforJ: La API subyacente de webforJ se vincula al hilo que creó la sesión.
- Afinidad del hilo de componentes: Los componentes de la interfaz de usuario mantienen un estado que no es seguro para varios hilos.
- Distribución de eventos: Todos los eventos de la interfaz de usuario se procesan secuencialmente en un solo hilo.
Este modelo de un solo hilo previene condiciones de carrera y mantiene un estado consistente para todos los componentes de la interfaz de usuario, pero crea desafíos al integrarse con tareas de computación asincrónica y de larga duración.
API RunLater
La API Environment.runLater() proporciona dos métodos para programar actualizaciones de la interfaz de usuario:
// Programa una tarea sin valor de retorno
public static PendingResult<Void> runLater(Runnable task)
// Programa una tarea que devuelve un valor
public static <T> PendingResult<T> runLater(Supplier<T> supplier)
Ambos métodos devuelven un PendingResult que rastrea la finalización de la tarea y proporciona acceso al resultado o a cualquier excepción que ocurriera.
Herencia del contexto de hilo
La herencia automática de contexto es una característica crítica de Environment.runLater(). Cuando un hilo que se ejecuta en un Environment crea hilos hijos, esos hijos heredan automáticamente la capacidad de usar runLater().
Cómo funciona la herencia
Cualquier hilo creado desde dentro de un hilo Environment tiene acceso automáticamente a ese Environment. Esta herencia ocurre automáticamente, por lo que no es necesario pasar ningún contexto o configurar nada.
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// Este hilo tiene contexto de Environment
// Los hilos hijo heredan el contexto automáticamente
executor.submit(() -> {
String data = fetchRemoteData();
// Puede usar runLater porque se heredó el contexto
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Hilos sin contexto
Los hilos creados fuera del contexto de Environment no pueden usar runLater() y lanzarán una IllegalStateException:
// Inicializador estático - sin contexto de Environment
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Lanza IllegalStateException
}).start();
}
// Hilos de temporizador del sistema - sin contexto de Environment
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Lanza IllegalStateException
}
}, 1000);
// Hilos de bibliotecas externas - sin contexto de Environment
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Lanza IllegalStateException
});
Comportamiento de ejecución
El comportamiento de ejecución de runLater() depende de qué hilo lo llame:
Desde el hilo de la interfaz de usuario
Cuando se llama desde el hilo Environment, las tareas se ejecutan sincrónicamente e inmediatamente:
button.onClick(e -> {
System.out.println("Antes: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Dentro: " + Thread.currentThread().getName());
return "completado";
});
System.out.println("Después: " + result.isDone()); // true
});
Con este comportamiento sincrónico, las actualizaciones de la interfaz de usuario desde los manejadores de eventos se aplican de inmediato y no incurren en ningún costo innecesario de encolado.
Desde hilos en segundo plano
Cuando se llama desde un hilo en segundo plano, las tareas se encolan para ejecución asincrónica:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Esto se ejecuta en el hilo ForkJoinPool
System.out.println("En segundo plano: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// Esto se ejecuta en el hilo de Environment
System.out.println("Actualización de UI: " + Thread.currentThread().getName());
statusLabel.setText("Procesamiento completo");
});
// result.isDone() sería falso aquí
// La tarea está encolada y se ejecutará asincrónicamente
});
}
webforJ procesa las tareas enviadas desde hilos en segundo plano en estricto orden FIFO, preservando la secuencia de operaciones incluso cuando se envían desde múltiples hilos de manera concurrente. Con esta garantía de orden, las actualizaciones de la interfaz de usuario se aplican en el orden exacto en el que fueron enviadas. Por lo tanto, si el hilo A envía la tarea 1 y luego el hilo B envía la tarea 2, la tarea 1 siempre se ejecutará antes que la tarea 2 en el hilo de la interfaz de usuario. El procesamiento de las tareas en orden FIFO previene inconsistencias en la interfaz de usuario.
Cancelación de tareas
El PendingResult devuelto por Environment.runLater() admite cancelación, lo que permite evitar que las tareas encoladas se ejecuten. Al cancelar tareas pendientes, puede evitar fugas de memoria y prevenir que operaciones de larga duración actualicen la interfaz de usuario después de que ya no son necesarias.
Cancelación básica
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Cancelar si aún no se ha ejecutado
if (!result.isDone()) {
result.cancel();
}
Manejo de múltiples actualizaciones
Al realizar operaciones de larga duración con actualizaciones frecuentes de la interfaz de usuario, realice un seguimiento de todos los resultados pendientes:
public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;
public void startTask() {
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;
final int progress = i;
PendingResult<Void> update = Environment.runLater(() -> {
progressBar.setValue(progress);
});
// Seguimiento para cancelación potencial
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Cancelar todas las actualizaciones de UI pendientes
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Manejo del ciclo de vida de los componentes
Cuando los componentes se destruyen (por ejemplo, durante la navegación), cancele todas las actualizaciones pendientes para prevenir fugas de memoria:
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// Cancelar todas las actualizaciones pendientes para evitar fugas de memoria
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Consideraciones de diseño
-
Requisito de contexto: Los hilos deben haber heredado un contexto de
Environment. Los hilos de bibliotecas externas, los temporizadores del sistema y los inicializadores estáticos no pueden usar esta API. -
Prevención de fugas de memoria: Siempre realice un seguimiento y cancele los objetos
PendingResulten los métodos del ciclo de vida del componente. Las lambdas encoladas capturan referencias a los componentes de la interfaz de usuario, lo que evita la recolección de basura si no se cancelan. -
Ejecución FIFO: Todas las tareas se ejecutan en estricto orden FIFO, independientemente de la importancia. No hay un sistema de prioridad.
-
Limitaciones de cancelación: La cancelación solo evita la ejecución de tareas encoladas. Las tareas que ya están en ejecución finalizarán normalmente.
Estudio de caso completo: LongTaskView
Lo siguiente es una implementación completa y lista para producción que demuestra todas las mejores prácticas para actualizaciones asincrónicas de la interfaz de usuario:
Análisis del estudio de caso
Esta implementación demuestra varios patrones críticos:
1. Manejo de pool de hilos
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Usa un ejecutor de hilo único para prevenir el agotamiento de recursos.
- Crea hilos demonio que no impedirán el cierre de la JVM.
2. Seguimiento de actualizaciones pendientes
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Cada llamada a Environment.runLater() se rastrea para permitir:
- Cancelación cuando el usuario hace clic en cancelar.
- Prevención de fugas de memoria en
onDestroy(). - Limpieza adecuada durante el ciclo de vida del componente.
3. Cancelación cooperativa
private volatile boolean isCancelled = false;
El hilo en segundo plano verifica esta bandera en cada iteración, permitiendo:
- Respuesta inmediata a la cancelación.
- Salida limpia del bucle.
- Prevención de actualizaciones adicionales de la UI.
4. Manejo del ciclo de vida
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reutiliza la lógica de cancelación.
currentTask = null;
executor.shutdown();
}
Crítico para prevenir fugas de memoria mediante:
- Cancelación de todas las actualizaciones de UI pendientes.
- Interrupción de hilos en ejecución.
- Apagado del ejecutor.
5. Pruebas de receptividad de la UI
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Clic #" + count + " - ¡La UI está receptiva!", Theme.GRAY);
});
Demuestra que el hilo de la UI permanece receptivo durante operaciones en segundo plano.