Skip to main content

Asynchronous Updates

25.02 Experimental
Java API

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.

Experimental API

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:

  1. webforJ API constraints: The underlying webforJ API binds to the thread that created the session
  2. Component thread affinity: UI components maintain state that's not thread-safe
  3. 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:

Environment.java
// 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

  1. Context requirement: Threads must have inherited an Environment context. External library threads, system timers, and static initializers can't use this API.

  2. 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.

  3. FIFO execution: All tasks execute in strict FIFO order regardless of importance. There's no priority system.

  4. 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:

LongTaskView.java
    startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Starting background task...");
progressBar.setValue(0);
resultField.setValue("");

// Reset cancelled flag and clear previous pending updates
isCancelled = false;
pendingUIUpdates.clear();

// Start background task with explicit executor
// Note: cancel(true) will interrupt the thread, causing Thread.sleep() to throw
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simulate long task with 100 steps
for (int i = 0; i <= 100; i++) {
// Check if cancelled
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("Task cancelled!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Task was cancelled", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 seconds total
} catch (InterruptedException e) {
// Thread was interrupted - exit immediately
Thread.currentThread().interrupt(); // Restore interrupted status
return;
}

// Do some calculation (deterministic for demo)
// Produces values between 0 and 1
result += Math.sin(i) * 0.5 + 0.5;

// Update progress from background thread
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Processing... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Final update with result (this code is only reached if the task completed without
// cancellation)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("Task completed!");
resultField.setValue("Result: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Background task finished!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

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.