From 4456fd24da244a80b5ed885276a73c5631708a95 Mon Sep 17 00:00:00 2001 From: Herberts Markuns Date: Thu, 6 Aug 2026 17:37:48 +0300 Subject: [PATCH 1/2] #2571 Update long running tasks doc to use CompletableFuture --- .../flow/advanced/long-running-tasks.adoc | 169 ++++++++++-------- 1 file changed, 94 insertions(+), 75 deletions(-) diff --git a/articles/flow/advanced/long-running-tasks.adoc b/articles/flow/advanced/long-running-tasks.adoc index 8df4743ea1..7392cd7bcf 100644 --- a/articles/flow/advanced/long-running-tasks.adoc +++ b/articles/flow/advanced/long-running-tasks.adoc @@ -91,28 +91,28 @@ The following example shows how the [methodname]`BackendService.longRunningTask( public class BackendService { @Async // <1> - public ListenableFuture longRunningTask() { // <2> + public CompletableFuture longRunningTask() { // <2> try { // Simulate a long running task Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } - return AsyncResult.forValue("Some result"); // <3> + return CompletableFuture.completedFuture("Some result"); // <3> } } ---- <1> [annotationname]`@Async` annotation to mark the method for asynchronous execution. -<2> The method now returns a [classname]`ListenableFuture` object. -<3> The method's return value is a [classname]`ListenableFuture` object that contains the result of the asynchronous task. +<2> The method now returns a [classname]`CompletableFuture` object. +<3> The method's return value is a [classname]`CompletableFuture` object that contains the result of the asynchronous task. Now the [methodname]`BackendService.longRunningTask()` method is annotated with the [annotationname]`@Async` annotation, and the long-running task is executed in a separate thread. -The [methodname]`BackendService.longRunningTask()` method now returns a [interfacename]`ListenableFuture` instead of a `String` (returning a [interfacename]`ListenableFuture` or a [interfacename]`CompletableFuture` is a requirement for any asynchronous service). -The [interfacename]`ListenableFuture` is a special type of [interfacename]`Future` that allows the caller to register a callback to be notified when the task is completed. +The [methodname]`BackendService.longRunningTask()` method now returns a [interfacename]`CompletableFuture` instead of a `String` (returning a [interfacename]`CompletableFuture` is a requirement for any asynchronous service). +The [interfacename]`CompletableFuture` is a special type of [interfacename]`Future` that allows the caller to register a callback to be notified when the task is completed. With these changes in place, you can change the UI to allow the user to start the long-running task and still be able to interact with the application. -Vaadin can then use the [interfacename]`ListenableFuture` and the [methodname]`UI.access()` method of <<{articles}/flow/advanced/server-push#, Server Push>> to notify the user when the task is completed. +Vaadin can then use the [interfacename]`CompletableFuture` and the [methodname]`UI.access()` method of <<{articles}/flow/advanced/server-push#, Server Push>> to notify the user when the task is completed. This is how [filename]`MainView.java` could look now: .`MainView.java` @@ -122,40 +122,46 @@ This is how [filename]`MainView.java` could look now: public class MainView extends VerticalLayout { public MainView(BackendService backendService) { - Button startButton = new Button("Start long-running task", clickEvent -> { - UI ui = clickEvent.getSource().getUI().orElseThrow(); // <1> - ListenableFuture future = backendService.longRunningTask(); - future.addCallback( - successResult -> updateUi(ui, "Task finished: " + successResult), // <2> - failureException -> updateUi(ui, "Task failed: " + failureException.getMessage()) // <3> - ); + var notificationSignal = new ValueSignal(null); // <1> + + var startButton = new Button("Start long-running task", clickEvent -> { + CompletableFuture future = backendService.longRunningTask(); + future.whenComplete((successResult, exception) -> { + if (exception == null) { + notificationSignal.set("Task finished: " + successResult); // <2> + } else { + notificationSignal.set("Task failed: " + exception.getMessage()); // <3> + } + notificationSignal.set(null); // <4> + }); }); - Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> { - Notification.show("UI isn't blocked!"); + Signal.effect(startButton, signal -> { // <5> + var value = notificationSignal.get(); + if (value != null) { + Notification.show(value); + } }); - add(startButton, isBlockedButton); - } + var isBlockedButton = new Button("Is UI blocked?", clickEvent -> + Notification.show("UI isn't blocked!") + ); - private void updateUi(UI ui, String result) { // <4> - ui.access(() -> { - Notification.show(result); - }); + add(startButton, isBlockedButton); } - } ---- -<1> Save the current UI in a local variable, so that you can use it later to update the UI through the [methodname]`UI.access()` method. -<2> The callback is called when the task is completed successfully. -<3> The callback is called if the task failed. -<4> The [methodname]`UI.access()` method is used to update the UI in a thread-safe manner through server-side push. +<1> Create a [classname]`ValueSignal` that will store the result message. +<2> Update signal value when the task is completed successfully. +<3> Update signal value when the task failed. +<4> Reset signal value to `null` so that next time message is updated, the [classname]`Signal` effect is triggered (it doesn't trigger for the same value). +<5> Create a [classname]`Signal` effect, that gets the value from `notificationSignal` and shows a notification, if it is not `null`. *You're still not done.* -For the above example to work as intended, you need two extra annotations for the [annotationname]`@Async` annotation and the [methodname]`UI.access()` method to work. +For the above example to work as intended, you need two extra annotations for the [annotationname]`@Async` annotation and the [classname]`Signal` effect to work. * For the [annotationname]`@Async` annotation, you need to add the [annotationname]`@EnableAsync` annotation to the application. -* For the [methodname]`UI.access()` method, you need to add the [annotationname]`@Push` annotation to the class implementing the [interfacename]`AppShellConfigurator` interface. +* For the [classname]`Signal` effect to work, you need to add the [annotationname]`@Push` annotation to the class implementing the [interfacename]`AppShellConfigurator` interface. You can make both changes in the same class as illustrated in the following [classname]`Application` class (which both extends [classname]`SpringBootServletInitializer` and implements [interfacename]`AppShellConfigurator`): @@ -188,36 +194,39 @@ public class MainView extends VerticalLayout { private ProgressBar progressBar = new ProgressBar(); // <1> public MainView(BackendService backendService) { + var notificationSignal = new ValueSignal(null); + var progressBar = new ProgressBar(); // <1> progressBar.setWidth("15em"); progressBar.setIndeterminate(true); progressBar.setVisible(false); // <2> - Button startButton = new Button("Start long-running task", clickEvent -> { - UI ui = clickEvent.getSource().getUI().orElseThrow(); - ListenableFuture future = backendService.longRunningTask(); - + var startButton = new Button("Start long-running task", clickEvent -> { + CompletableFuture future = backendService.longRunningTask(); progressBar.setVisible(true); // <3> - - future.addCallback( - successResult -> updateUi(ui, "Task finished: " + successResult), - failureException -> updateUi(ui, "Task failed: " + failureException.getMessage()) - ); + future.whenComplete((successResult, exception) -> { + if (exception == null) { + notificationSignal.set("Task finished: " + successResult); + } else { + notificationSignal.set("Task failed: " + exception.getMessage()); + } + notificationSignal.set(null); + }); }); - Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> { - Notification.show("UI isn't blocked!"); + Signal.effect(startButton, signal -> { + var value = notificationSignal.get(); + if (value != null) { + progressBar.setVisible(false); // <4> + Notification.show(value); + } }); - add(startButton, progressBar, isBlockedButton); - } + var isBlockedButton = new Button("Is UI blocked?", clickEvent -> + Notification.show("UI isn't blocked!") + ); - private void updateUi(UI ui, String result) { - ui.access(() -> { - Notification.show(result); - progressBar.setVisible(false); // <4> - }); + add(startButton, progressBar, isBlockedButton); } - } ---- <1> First, create a [classname]`ProgressBar` object. @@ -234,7 +243,7 @@ image::images/vaadin-progress-bar-no-cancel.gif[Long-Running Task with ProgressB For your task to be cancellable, the following conditions must be met: . Your [annotationname]`@Async` method must return a [interfacename]`Future`. -. The running task must be https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/concurrent/Future.html#cancel(boolean)[cancellable]. +. The running task must be https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Future.html#cancel(boolean)[cancellable]. The modified [classname]`MainView` class below shows how to add a [classname]`Button` to cancel the long-running task. @@ -248,47 +257,57 @@ public class MainView extends VerticalLayout { private Button cancelButton = new Button("Cancel task execution"); public MainView(BackendService backendService) { + var notificationSignal = new ValueSignal(null); + + var progressBar = new ProgressBar(); progressBar.setWidth("15em"); progressBar.setIndeterminate(true); - progressBar.setVisible(false); - cancelButton.setVisible(false); // <1> - Button startButton = new Button("Start long-running task", clickEvent -> { - UI ui = clickEvent.getSource().getUI().orElseThrow(); - ListenableFuture future = backendService.longRunningTask(); + var cancelWrapper = new Div(); // <1> + cancelWrapper.setVisible(false); + var startButton = new Button("Start long-running task", clickEvent -> { + CompletableFuture future = backendService.longRunningTask(); progressBar.setVisible(true); - cancelButton.setVisible(true); // <2> - cancelButton.addClickListener(e -> future.cancel(true)); // <3> - future.addCallback( - successResult -> updateUi(ui, "Task finished: " + successResult), - failureException -> updateUi(ui, "Task failed: " + failureException.getMessage()) - ); + var cancelButton = new Button("Cancel", // <2> + e -> future.cancel(true)); // <3> + cancelWrapper.setVisible(true); + cancelWrapper.add(cancelButton); + + future.whenComplete((successResult, exception) -> { + if (exception == null) { + notificationSignal.set("Task finished: " + successResult); + } else { + notificationSignal.set("Task failed: " + exception.getMessage()); + } + notificationSignal.set(null); + }); }); - Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> { - Notification.show("UI isn't blocked!"); + Signal.effect(startButton, signal -> { + var value = notificationSignal.get(); + if (value != null) { + progressBar.setVisible(false); + cancelWrapper.setVisible(false); // <4> + cancelWrapper.removeAll(); + Notification.show(value); + } }); - add(startButton, new HorizontalLayout(progressBar, cancelButton), isBlockedButton); - } + var isBlockedButton = new Button("Is UI blocked?", clickEvent -> + Notification.show("UI isn't blocked!") + ); - private void updateUi(UI ui, String result) { - ui.access(() -> { - Notification.show(result); - progressBar.setVisible(false); - cancelButton.setVisible(false); // <4> - }); + add(startButton, new HorizontalLayout(progressBar, cancelWrapper), isBlockedButton); } - } ---- -<1> Like the [classname]`ProgressBar`, hide the *Cancel* [classname]`Button` by default. -<2> Show the *Cancel* [classname]`Button` when the task is started. -<3> The [classname]`Future` representing the long-running task is canceled when the *Cancel* [classname]`Button` is clicked. -<4> When the task is completed or canceled, hide the cancel [classname]`Button`. +<1> Create wrapper [classname]`Div`, where the cancel button would be placed. +<2> Create [classname]`Button` for task cancellation, and it to the wrapper. +<3> On [classname]`Button` click event, cancel the task. +<4> When completed, hide the Cancel [classname]`Button` wrapper, and remove all of its children. Here is the animation of the [classname]`MainView` with a *Cancel* [classname]`Button`. From c5ae5317f7b30f1c418bad31f406a7630175df22 Mon Sep 17 00:00:00 2001 From: Herberts Markuns Date: Fri, 7 Aug 2026 10:19:31 +0300 Subject: [PATCH 2/2] #2571 Replace Signal example to ui access example --- .../flow/advanced/long-running-tasks.adoc | 103 ++++++++---------- 1 file changed, 46 insertions(+), 57 deletions(-) diff --git a/articles/flow/advanced/long-running-tasks.adoc b/articles/flow/advanced/long-running-tasks.adoc index 7392cd7bcf..d17a6f9fa1 100644 --- a/articles/flow/advanced/long-running-tasks.adoc +++ b/articles/flow/advanced/long-running-tasks.adoc @@ -122,46 +122,42 @@ This is how [filename]`MainView.java` could look now: public class MainView extends VerticalLayout { public MainView(BackendService backendService) { - var notificationSignal = new ValueSignal(null); // <1> - var startButton = new Button("Start long-running task", clickEvent -> { + var ui = clickEvent.getSource().getUI().orElseThrow(); CompletableFuture future = backendService.longRunningTask(); future.whenComplete((successResult, exception) -> { if (exception == null) { - notificationSignal.set("Task finished: " + successResult); // <2> + updateUi(ui, "Task finished: " + successResult); // <2> } else { - notificationSignal.set("Task failed: " + exception.getMessage()); // <3> + updateUi(ui, "Task failed: " + exception.getMessage()); // <3> } - notificationSignal.set(null); // <4> }); }); - Signal.effect(startButton, signal -> { // <5> - var value = notificationSignal.get(); - if (value != null) { - Notification.show(value); - } - }); - var isBlockedButton = new Button("Is UI blocked?", clickEvent -> Notification.show("UI isn't blocked!") ); add(startButton, isBlockedButton); } + + private void updateUi(UI ui, String result) { // <4> + ui.access(() -> { + Notification.show(result); + }); + } } ---- -<1> Create a [classname]`ValueSignal` that will store the result message. -<2> Update signal value when the task is completed successfully. -<3> Update signal value when the task failed. -<4> Reset signal value to `null` so that next time message is updated, the [classname]`Signal` effect is triggered (it doesn't trigger for the same value). -<5> Create a [classname]`Signal` effect, that gets the value from `notificationSignal` and shows a notification, if it is not `null`. +<1> Save the current UI in a local variable, so that you can use it later to update the UI through the [methodname]`UI.access()` method. +<2> The callback is called when the task is completed successfully. +<3> The callback is called if the task failed. +<4> The [methodname]`UI.access()` method is used to update the UI in a thread-safe manner through server-side push. *You're still not done.* -For the above example to work as intended, you need two extra annotations for the [annotationname]`@Async` annotation and the [classname]`Signal` effect to work. +For the above example to work as intended, you need two extra annotations for the [annotationname]`@Async` annotation and the [methodname]`UI.access()` method to work. * For the [annotationname]`@Async` annotation, you need to add the [annotationname]`@EnableAsync` annotation to the application. -* For the [classname]`Signal` effect to work, you need to add the [annotationname]`@Push` annotation to the class implementing the [interfacename]`AppShellConfigurator` interface. +* For the [methodname]`UI.access()` method to work, you need to add the [annotationname]`@Push` annotation to the class implementing the [interfacename]`AppShellConfigurator` interface. You can make both changes in the same class as illustrated in the following [classname]`Application` class (which both extends [classname]`SpringBootServletInitializer` and implements [interfacename]`AppShellConfigurator`): @@ -194,39 +190,38 @@ public class MainView extends VerticalLayout { private ProgressBar progressBar = new ProgressBar(); // <1> public MainView(BackendService backendService) { - var notificationSignal = new ValueSignal(null); - var progressBar = new ProgressBar(); // <1> progressBar.setWidth("15em"); progressBar.setIndeterminate(true); progressBar.setVisible(false); // <2> var startButton = new Button("Start long-running task", clickEvent -> { + var ui = clickEvent.getSource().getUI().orElseThrow(); CompletableFuture future = backendService.longRunningTask(); + progressBar.setVisible(true); // <3> + future.whenComplete((successResult, exception) -> { if (exception == null) { - notificationSignal.set("Task finished: " + successResult); + updateUi(ui, "Task finished: " + successResult); // <2> } else { - notificationSignal.set("Task failed: " + exception.getMessage()); + updateUi(ui, "Task failed: " + exception.getMessage()); // <3> } - notificationSignal.set(null); }); }); - Signal.effect(startButton, signal -> { - var value = notificationSignal.get(); - if (value != null) { - progressBar.setVisible(false); // <4> - Notification.show(value); - } + var isBlockedButton = new Button("Is UI blocked?", clickEvent -> { + Notification.show("UI isn't blocked!"); }); - var isBlockedButton = new Button("Is UI blocked?", clickEvent -> - Notification.show("UI isn't blocked!") - ); - add(startButton, progressBar, isBlockedButton); } + + private void updateUi(UI ui, String result) { + ui.access(() -> { + Notification.show(result); + progressBar.setVisible(false); // <4> + }); + } } ---- <1> First, create a [classname]`ProgressBar` object. @@ -254,54 +249,48 @@ The modified [classname]`MainView` class below shows how to add a [classname]`Bu public class MainView extends VerticalLayout { private ProgressBar progressBar = new ProgressBar(); - private Button cancelButton = new Button("Cancel task execution"); + private Div cancelWrapper = new Div(); // <1> public MainView(BackendService backendService) { - var notificationSignal = new ValueSignal(null); - - var progressBar = new ProgressBar(); progressBar.setWidth("15em"); progressBar.setIndeterminate(true); progressBar.setVisible(false); - - var cancelWrapper = new Div(); // <1> cancelWrapper.setVisible(false); var startButton = new Button("Start long-running task", clickEvent -> { + var ui = clickEvent.getSource().getUI().orElseThrow(); CompletableFuture future = backendService.longRunningTask(); progressBar.setVisible(true); - var cancelButton = new Button("Cancel", // <2> - e -> future.cancel(true)); // <3> + var cancelButton = new Button("Cancel task execution", // <2> + e -> future.cancel(true)); // <3> cancelWrapper.setVisible(true); cancelWrapper.add(cancelButton); future.whenComplete((successResult, exception) -> { if (exception == null) { - notificationSignal.set("Task finished: " + successResult); + updateUi(ui, "Task finished: " + successResult); } else { - notificationSignal.set("Task failed: " + exception.getMessage()); + updateUi(ui, "Task failed: " + exception.getMessage()); } - notificationSignal.set(null); }); }); - Signal.effect(startButton, signal -> { - var value = notificationSignal.get(); - if (value != null) { - progressBar.setVisible(false); - cancelWrapper.setVisible(false); // <4> - cancelWrapper.removeAll(); - Notification.show(value); - } + var isBlockedButton = new Button("Is UI blocked?", clickEvent -> { + Notification.show("UI isn't blocked!"); }); - var isBlockedButton = new Button("Is UI blocked?", clickEvent -> - Notification.show("UI isn't blocked!") - ); - add(startButton, new HorizontalLayout(progressBar, cancelWrapper), isBlockedButton); } + + private void updateUi(UI ui, String result) { + ui.access(() -> { + Notification.show(result); + progressBar.setVisible(false); + cancelWrapper.setVisible(false); // <4> + cancelWrapper.removeAll(); + }); + } } ---- <1> Create wrapper [classname]`Div`, where the cancel button would be placed.