Asynchronous Updates
The Environment.runLater()
API provides a mechanism for safely updating the UI from background threads in webforJ applications. This experimental feature enables asynchronous operations while maintaining thread safety for UI modifications.
This API is marked as experimental since 25.02 and may change in future releases. The API signature, behavior, and performance characteristics are subject to modification.
Understanding the thread model
webforJ enforces a strict threading model where all UI operations must occur on the Environment
thread. This restriction exists because:
- webforJ API constraints: The underlying webforJ API binds to the thread that created the session
- Component thread affinity: UI components maintain state that's not thread-safe
- Event dispatch: All UI events are processed sequentially on a single thread
This single-threaded model prevents race conditions and maintains a consistent state for all UI components, but creates challenges when integrating with asynchronous, long-running computation tasks.
RunLater
API
The Environment.runLater()
API provides two methods for scheduling UI updates:
// Schedule a task with no return value
public static PendingResult<Void> runLater(Runnable task)
// Schedule a task that returns a value
public static <T> PendingResult<T> runLater(Supplier<T> supplier)
Both methods return a PendingResult
that tracks task completion and provides access to the result or any exceptions that occurred.
Thread context inheritance
Automatic context inheritance is a critical feature of Environment.runLater()
. When a thread running in an Environment
creates child threads, those children automatically inherit the ability to use runLater()
.
How inheritance works
Any thread created from within an Environment
thread automatically has access to that Environment
. This inheritance happens automatically, so you don't need to pass any context or configure anything.
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// This thread has Environment context
// Child threads inherit the context automatically
executor.submit(() -> {
String data = fetchRemoteData();
// Can use runLater because context was inherited
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Threads without context
Threads created outside the Environment
context can't use runLater()
and will throw an IllegalStateException
:
// Static initializer - no Environment context
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Throws IllegalStateException
}).start();
}
// System timer threads - no Environment context
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Throws IllegalStateException
}
}, 1000);
// External library threads - no Environment context
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Throws IllegalStateException
});
Execution behavior
The execution behavior of runLater()
depends on which thread calls it:
From the UI thread
When called from the Environment
thread itself, tasks execute synchronously and immediately:
button.onClick(e -> {
System.out.println("Before: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Inside: " + Thread.currentThread().getName());
return "completed";
});
System.out.println("After: " + result.isDone()); // true
});
With this synchronous behavior, UI updates from event handlers are applied immediately and don't incur any unnecessary queueing overhead.
From background threads
When called from a background thread, tasks are queued for asynchronous execution:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// This runs on ForkJoinPool thread
System.out.println("Background: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// This runs on 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 processes tasks submitted from background threads in strict FIFO order, preserving the sequence of operations even when submitted from multiple threads concurrently. With this ordering guarantee, UI updates are applied in the exact order they were submitted. So if thread A submits task 1, and then thread B submits task 2, task 1 will always execute before task 2 on the UI thread. Processing tasks in FIFO order prevents inconsistencies in the UI.
Task cancellation
The PendingResult
returned by Environment.runLater()
supports cancellation, allowing you to prevent queued tasks from executing. By cancelling pending tasks, you can avoid memory leaks and prevent long-running operations from updating the UI after they're no longer needed.
Basic cancellation
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Cancel if not yet executed
if (!result.isDone()) {
result.cancel();
}
Managing multiple updates
When performing long-running operations with frequent UI updates, track all pending results:
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);
});
// 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();
}
}
Component lifecycle management
When components are destroyed (e.g., during navigation), cancel all pending updates to prevent memory leaks:
@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();
}
}
Design considerations
-
Context requirement: Threads must have inherited an
Environment
context. External library threads, system timers, and static initializers can't use this API. -
Memory leak prevention: Always track and cancel
PendingResult
objects in component lifecycle methods. Queued lambdas capture references to UI components, preventing garbage collection if not cancelled. -
FIFO execution: All tasks execute in strict FIFO order regardless of importance. There's no priority system.
-
Cancellation limitations: Cancellation only prevents execution of queued tasks. Tasks already executing will complete normally.
Complete case study: LongTaskView
The following is a complete, production-ready implementation demonstrating all best practices for asynchronous UI updates:
Case study analysis
This implementation demonstrates several critical patterns:
1. Thread pool management
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Uses a single thread executor to prevent resource exhaustion
- Creates daemon threads that won't prevent JVM shutdown
2. Tracking pending updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Every Environment.runLater()
call is tracked to enable:
- Cancellation when the user clicks cancel
- Memory leak prevention in
onDestroy()
- Proper cleanup during component lifecycle
3. Cooperative cancellation
private volatile boolean isCancelled = false;
The background thread checks this flag at each iteration, enabling:
- Immediate response to cancellation
- Clean exit from the loop
- Prevention of further UI updates
4. Lifecycle management
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reuses cancellation logic
currentTask = null;
executor.shutdown();
}
Critical for preventing memory leaks by:
- Cancelling all pending UI updates
- Interrupting running threads
- Shutting down the executor
5. UI responsiveness testing
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Click #" + count + " - UI is responsive!", Theme.GRAY);
});
Demonstrates that the UI thread remains responsive during background operations.