-
Notifications
You must be signed in to change notification settings - Fork 0
How to Use Asynchronous Updates in Vaadin 8
This guide shows a practical pattern for running background work in Vaadin 8 and safely updating the UI using Vaadin Push and a shared ExecutorService. The examples come from the statistics dashboard in this project (StatsPresenter, StatsView, Utils.access, AccessTask, and VaadinCreateUI).
The same pattern can be reused for long-running tasks like report generation, batch imports, or slow external API calls.
Vaadin 8 UIs must be explicitly configured for push when you want to update them from a background thread.
In this application, the main UI is annotated with @Push and exposes a per-UI executor:
@Push(transport = Transport.WEBSOCKET_XHR)
public class VaadinCreateUI extends UI implements EventBusListener {
private ExecutorService executor;
public ExecutorService getExecutor() {
if (executor == null) {
executor = Executors.newSingleThreadExecutor(
Thread.ofVirtual().name("user-job").factory());
executor.execute(() -> {
if (!executor.isShutdown()) {
User user = (User) getSession().getSession()
.getAttribute(CurrentUser.CURRENT_USER_SESSION_ATTRIBUTE_KEY);
if (user != null) {
String userId = String.format("[%s/%s]",
user.getRole(), user.getName());
MDC.put("userId", userId);
}
}
});
}
return executor;
}
@Override
public void detach() {
super.detach();
shutdownExecutor();
}
}Key ideas:
-
Push enabled:
@Pushallows background threads to push UI changes to the browser. -
One executor per UI:
getExecutor()lazily creates a single-thread executor for this UI. -
Virtual threads: the executor uses
Thread.ofVirtual()to keep background tasks lightweight. -
Cleanup:
detach()shuts down the executor when the UI is closed.
Using a single-thread executor per UI also caps the amount of parallel background work each user can trigger, preventing a single session from flooding the server with jobs and improving overall scalability.
Vaadin requires all UI changes to run in the UI’s lock via UI.access(...). This project wraps that pattern in a utility method and an error-handling task.
public final class Utils {
public static void access(@Nullable UI ui, Runnable command) {
if (ui != null) {
ui.access(new AccessTask(command));
} else {
logger.warn("No UI available for pushing updates.");
}
}
}
public class AccessTask implements ErrorHandlingRunnable {
private final Runnable command;
public AccessTask(Runnable command) {
this.command = command;
}
@Override
public void run() {
command.run();
}
@Override
public void handleError(Exception exception) {
if (exception instanceof UIDetachedException) {
logger.info("Browser window was closed while pushing updates.");
} else {
logger.error("Error while pushing updates", exception);
}
}
}Benefits of this pattern:
- All background callbacks call
Utils.access(ui, ...), notUI.access(...)directly. - If the UI is detached (browser closed),
UIDetachedExceptionis logged as info, not as a test-breaking error. - If any other exception occurs during a push, it’s logged with stack trace.
The statistics dashboard fetches data and computes aggregates in a background thread, then updates the view via push.
public class StatsPresenter implements EventBusListener, Serializable {
private final StatsView view;
private CompletableFuture<Void> future;
private ProductDataService service = VaadinCreateUI.get().getProductService();
private ExecutorService executor = VaadinCreateUI.get().getExecutor();
record ProductData(Collection<Product> products,
Collection<Category> categories) {}
public StatsPresenter(StatsView view) {
this.view = view;
getEventBus().registerEventBusListener(this);
}
private CompletableFuture<ProductData> loadProductsAsync() {
var productService = getService();
return CompletableFuture.supplyAsync(
() -> new ProductData(productService.getAllProducts(),
productService.getAllCategories()),
getExecutor());
}
public void requestUpdateStats() {
future = loadProductsAsync()
.thenAccept(this::calculateStatistics)
.whenComplete((result, throwable) -> future = null);
}
private void calculateStatistics(ProductData productData) {
var stats = new ProductStatistics(
StatsUtils.calculateAvailabilityStats(productData.products()),
StatsUtils.calculateCategoryStats(productData.categories(),
productData.products()),
StatsUtils.calculatePriceStats(productData.products()));
view.updateStatsAsync(stats);
}
public void cancelUpdateStats() {
getEventBus().unregisterEventBusListener(this);
if (future != null) {
future.cancel(true);
future = null;
}
}
}Notable points:
-
Background work (
loadProductsAsync) is offloaded to the per-UI executor. -
Computation (
calculateStatistics) runs on that background thread. -
UI update is delegated to the view via
view.updateStatsAsync(stats). - Cancellation is supported when the view is detached.
An important detail is that no backend or business logic runs inside the UI access block: all fetching and aggregation happen in the presenter on a background thread, and the view’s updateStatsAsync(...) only applies precomputed values via Utils.access(ui, ...). This preserves MVP separation of concerns and keeps the UI lock and request thread usage very short.
The view itself never blocks in the constructor; it only triggers background work when navigation is complete and updates itself through Utils.access.
public class StatsView extends VerticalLayout implements VaadinCreateView {
private final StatsPresenter presenter = new StatsPresenter(this);
@Override
public void enter(ViewChangeEvent event) {
openingView(VIEW_NAME);
presenter.requestUpdateStats();
}
}Calling the backend from enter(...) (instead of in the view constructor) ensures that navigation has completed, access control has verified the user may see this view, and the UI is fully initialized before any background work starts, which avoids wasted work for views the user never actually enters.
public void updateStatsAsync(ProductStatistics stats) {
Utils.access(ui, () -> {
if (isAttached()) {
updateAvailabilityChart(stats.availabilityStats());
updateCategoryChart(stats.categoryStats());
updatePriceChart(stats.priceStats());
availabilityChart.drawChart();
categoryChart.drawChart();
priceChart.drawChart();
dashboard.addStyleName("loaded");
}
});
}
public void setLoadingAsync() {
Utils.access(ui, () -> {
dashboard.removeStyleName("loaded");
categoryChart.getConfiguration().removeyAxes();
});
}Key practices:
- All chart updates and UI state changes happen inside
Utils.access(ui, ...). -
isAttached()is checked before applying state to avoid race conditions with detaching views. - A simple CSS flag (
loaded) is used to control client-side loading indicators.
@Override
public void attach() {
super.attach();
ui = getUI();
resizeListener = ui.getPage().addBrowserWindowResizeListener(
e -> JavaScript.eval("vaadin.forceLayout()"));
}
@Override
public void detach() {
super.detach();
resizeListener.remove();
presenter.cancelUpdateStats();
ui = null;
}The view:
- Remembers its
UIonly while attached. - Cancels any outstanding statistics fetch when detached.
The UI reference is captured in attach() because getUI() is not fully thread safe and should not be looked up from arbitrary background callbacks. By cancelling the CompletableFuture in detach(), the presenter stops any in-flight backend work once the view is no longer visible, conserving server resources. Detach is triggered both when navigating away from the view and when the browser tab is closed; in the latest Vaadin 8 versions the UI is closed immediately on tab close instead of waiting for multiple missed heartbeats.
The combination used in this application is a solid, reusable pattern for asynchronous updates in Vaadin 8:
-
Enable Push and per-UI executor
- Annotate the UI with
@Push. - Provide
getExecutor()that returns a sharedExecutorService(virtual thread pool recommended). - Shut down the executor in
detach().
- Annotate the UI with
-
Centralize UI access and error handling
- Wrap
UI.accessin a helper likeUtils.access(ui, command). - Use an
ErrorHandlingRunnable(AccessTask) to logUIDetachedExceptioncleanly.
- Wrap
-
Keep heavy work off the UI thread
- In presenters, use
CompletableFuture.supplyAsync(..., getExecutor())for I/O and CPU-heavy work. - Pass results to the view using a small DTO/record.
- In presenters, use
-
Update the view only on the UI thread
- Expose methods like
updateStatsAsync(...)andsetLoadingAsync()in the view. - Inside those methods, use
Utils.access(ui, ...)to apply changes.
- Expose methods like
-
Handle lifecycle and cancellation
- Start background work in
enter(...), not in constructors. - Cancel pending operations and unregister listeners in
detach().
- Start background work in
Following this pattern keeps your Vaadin 8 application:
- Responsive (no long blocks on the UI thread)
- Robust (safe handling of closed browser windows)
- Observable (executor threads carry user information in logging MDC)
- Reusable (presenters encapsulate async logic, views focus on rendering)