Actualizaciones asíncronas
La API Environment.runLater() proporciona un mecanismo para actualizar la interfaz de usuario de forma segura desde subprocesos en segundo plano en aplicaciones webforJ. Esta función experimental permite realizar operaciones asíncronas y, al mismo tiempo, mantener la seguridad de los subprocesos al modificar 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."
Comprender el modelo de subprocesos
webforJ impone un modelo de subprocesos estricto según el cual todas las operaciones de la interfaz de usuario deben realizarse en el subproceso Environment. Esta restricción existe porque:
- Restricciones de la API de webforJ: La API subyacente de webforJ se vincula al subproceso que creó la sesión.
- Afinidad de los componentes con el subproceso: Los componentes de la interfaz de usuario mantienen un estado que no es seguro para los subprocesos.
- Despacho de eventos: Todos los eventos de la interfaz de usuario se procesan secuencialmente en un solo subproceso.
Este modelo de un solo subproceso evita las condiciones de carrera y mantiene un estado coherente para todos los componentes de la interfaz de usuario, pero plantea dificultades al integrarse con tareas de cálculo asíncronas de larga duración.
Cada sesión de usuario tiene su propio Environment y su propio subproceso Environment. Un subproceso en segundo plano no pertenece a ninguna sesión, por lo que se le debe indicar qué Environment debe actualizar.
API RunLater
La API Environment.runLater() proporciona dos métodos para programar actualizaciones de la interfaz de usuario. Ambos reciben el Environment de destino como primer argumento:
// Schedule a task with no return value
public static PendingResult<Void> runLater(Environment target, Runnable task)
// Schedule a task that returns a value
public static <T> PendingResult<T> runLater(Environment target, Supplier<T> supplier)
Ambos métodos devuelven un PendingResult que realiza un seguimiento de la finalización de la tarea y proporciona acceso al resultado o a las excepciones que se hayan producido.
Las variantes runLater(Runnable) y runLater(Supplier) deducen el destino a partir del subproceso que creó el subproceso que realiza la llamada. En un grupo de subprocesos compartido por varios usuarios, cada tarea se ejecuta en el Environment del usuario cuya llamada creó ese subproceso del grupo. Estas variantes están obsoletas desde la versión 26.10 y está previsto eliminarlas en la próxima versión principal. Pasa siempre el destino.
Pasar el destino
Environment.getCurrent() devuelve el Environment del subproceso actual. En el subproceso Environment, que es donde se ejecutan los constructores, los controladores de eventos y los métodos del ciclo de vida, devuelve el Environment de la sesión. En un subproceso en segundo plano devuelve null.
Lee el destino allí donde esté disponible, antes de que comience el trabajo en segundo plano, y pásalo al trabajo:
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// The constructor runs on the Environment thread
Environment env = Environment.getCurrent();
executor.submit(() -> {
String data = fetchRemoteData();
Environment.runLater(env, () -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Como el destino se pasa como argumento, la llamada funciona desde cualquier subproceso: uno creado por la vista, un subproceso de Spring @Async, el ForkJoinPool común, un subproceso de temporizador o un subproceso de devolución de llamada de un cliente HTTP.
Leer el destino en el subproceso equivocado
El único error que debes evitar es leer Environment.getCurrent() dentro del trabajo en segundo plano. Allí devuelve null y runLater() lanza una NullPointerException:
executor.submit(() -> {
String data = fetchRemoteData();
// Wrong: getCurrent() returns null on a background thread
Environment.runLater(Environment.getCurrent(), () -> dataLabel.setText(data));
});
Lo mismo se aplica a los inicializadores estáticos, los subprocesos de java.util.Timer y las devoluciones de llamada de bibliotecas externas. Ninguno de ellos tiene un Environment, por lo que el destino debe obtenerse de un campo, un parámetro o una variable local capturada cuyo valor se haya asignado en el subproceso Environment.
Comportamiento de ejecución
El comportamiento de ejecución de runLater() depende del subproceso desde el que se llama:
Desde el subproceso de la interfaz de usuario
Cuando se llama desde el propio subproceso Environment del destino, las tareas se ejecutan de forma síncrona e inmediata:
button.onClick(e -> {
Environment env = Environment.getCurrent();
System.out.println("Before: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(env, () -> {
System.out.println("Inside: " + Thread.currentThread().getName());
return "completed";
});
System.out.println("After: " + result.isDone()); // true
});
Gracias a este comportamiento síncrono, las actualizaciones de la interfaz de usuario desde los controladores de eventos se aplican de inmediato y no generan una sobrecarga innecesaria por el uso de colas.
Desde subprocesos en segundo plano
Cuando se llama desde cualquier otro subproceso, las tareas se añaden a una cola para ejecutarse de forma asíncrona:
@Override
public void onDidCreate() {
Environment env = Environment.getCurrent();
CompletableFuture.runAsync(() -> {
// This runs on a ForkJoinPool thread
System.out.println("Background: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(env, () -> {
// This runs on the Environment thread
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Processing complete");
});
// result.isDone() would be false here
// The task is queued and will execute asynchronously
});
}
webforJ procesa las tareas enviadas desde subprocesos en segundo plano en estricto orden FIFO, conservando la secuencia de las operaciones incluso cuando se envían simultáneamente desde varios subprocesos. Gracias a esta garantía de orden, las actualizaciones de la interfaz de usuario se aplican exactamente en el orden en que se enviaron. Por lo tanto, si el subproceso A envía la tarea 1 y luego el subproceso B envía la tarea 2, la tarea 1 siempre se ejecutará antes que la tarea 2 en el subproceso de la interfaz de usuario. Procesar las tareas en orden FIFO evita inconsistencias en la interfaz.
Cancelación de tareas
El PendingResult que devuelve Environment.runLater() admite la cancelación, lo que permite impedir la ejecución de las tareas en cola. Al cancelar tareas pendientes, puedes evitar pérdidas de memoria e impedir que operaciones de larga duración actualicen la interfaz de usuario cuando ya no sea necesario.
Cancelación básica
PendingResult<Void> result = Environment.runLater(env, () -> {
updateUI();
});
// Cancel if not yet executed
if (!result.isDone()) {
result.cancel();
}
Administrar varias actualizaciones
Al realizar operaciones de larga duración con actualizaciones frecuentes de la interfaz de usuario, haz 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() {
Environment env = Environment.getCurrent();
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;
final int progress = i;
PendingResult<Void> update = Environment.runLater(env, () -> {
progressBar.setValue(progress);
});
// Track for potential cancellation
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Cancel all pending UI updates
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Administración del ciclo de vida de los componentes
Cuando se destruyen componentes (por ejemplo, durante la navegación), cancela todas las actualizaciones pendientes para evitar pérdidas de memoria:
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// Cancel all pending updates to prevent memory leaks
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Consideraciones de diseño
-
Requisito del destino: El
Environmentde destino se obtiene conEnvironment.getCurrent()en el subprocesoEnvironmenty se pasa al trabajo en segundo plano. Si se lee desde un subproceso en segundo plano, devuelvenull. -
Prevención de pérdidas de memoria: Haz siempre un seguimiento de los objetos
PendingResulty cancélalos en los métodos del ciclo de vida de los componentes. Las lambdas en cola capturan referencias a los componentes de la interfaz de usuario e impiden que el recolector de basura los libere si no se cancelan. -
Ejecución FIFO: Todas las tareas se ejecutan en estricto orden FIFO, independientemente de su importancia. No hay un sistema de prioridades.
-
Limitaciones de la cancelación: La cancelación solo impide la ejecución de las tareas en cola. Las tareas que ya se están ejecutando finalizarán con normalidad.
Caso práctico completo: LongTaskView
A continuación se muestra una implementación completa, lista para producción, que demuestra las prácticas recomendadas para las actualizaciones asíncronas de la interfaz de usuario:
Análisis del caso práctico
Esta implementación muestra varios patrones esenciales:
1. Administración del grupo de subprocesos
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Usa un ejecutor de un solo subproceso para evitar el agotamiento de recursos.
- Crea subprocesos daemon que no impedirán el cierre de la JVM.
2. Asignar un nombre al destino
Environment env = Environment.getCurrent();
startLongTask() se ejecuta en el subproceso Environment, por lo que obtiene allí el destino y la tarea en segundo plano lo pasa a cada llamada a Environment.runLater(). El mismo código funciona sin cambios con un grupo de subprocesos compartido por toda la aplicación.
3. Seguimiento de las actualizaciones pendientes
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Se realiza un seguimiento de cada llamada a Environment.runLater() para permitir:
- Cancelarla cuando el usuario hace clic en Cancelar.
- Evitar pérdidas de memoria en
onDestroy(). - Realizar una limpieza adecuada durante el ciclo de vida del componente.
4. Cancelación cooperativa
private volatile boolean isCancelled = false;
El subproceso en segundo plano comprueba este indicador en cada iteración, lo que permite:
- Responder de inmediato a la cancelación.
- Salir del bucle limpiamente.
- Evitar nuevas actualizaciones de la interfaz de usuario.
5. Administración del ciclo de vida
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reuses cancellation logic
currentTask = null;
executor.shutdown();
}
Es esencial para evitar pérdidas de memoria mediante:
- La cancelación de todas las actualizaciones de la interfaz de usuario pendientes.
- La interrupción de los subprocesos en ejecución.
- El cierre del ejecutor.
6. Prueba de la capacidad de respuesta de la interfaz de usuario
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Click #" + count + " - UI is responsive!", Theme.GRAY);
});
Demuestra que el subproceso de la interfaz de usuario sigue respondiendo durante las operaciones en segundo plano.


