Open mrgreywater opened 1 year ago
There are many options to do this. Very often async operations start by clicking some button, that triggers fetching data etc. Thus one handy approach to reduce the boiler plate is to create AsyncButton that wraps the async operation. I have an example of such thing here:
https://gist.github.com/TatuLund/ea689f270cdb2483a80c4088e0e77106
public AsyncButtonView() {
Grid<String> grid = new Grid<>();
grid.addColumn(item -> item.toString()).setHeader("Strings");
// Button takes executor and two callbacks, the task and the update callbacks,
// the first one is supplied to background thread and the later one is run in access
AsyncButton<List<String>> button = new AsyncButton<>("Click", executor,
() -> getItems(), items -> grid.setItems(items));
button.addClickListener(e -> {
Notification.show("Loading ...");
});
add(button, grid);
}
public List<String> getItems() {
try {
// Simulating a long task
Thread.sleep(5000);
} catch (InterruptedException e) {
}
return IntStream.range(0, 10000)
.mapToObj(i -> "String " + random.nextInt(10000))
.collect(Collectors.toList());
}
There is another piece of prior art on the same topic here: https://vaadin.com/directory/component/async-manager
AsyncManager.register(this, asyncTask -> {
Thread.sleep(2000);
asyncTask.push(() -> add(new Label("ASYNC TASK: 2 seconds has passed")));
});
@TatuLund Thanks for your research. As far as the AsyncButton, it only has very limited usage. It only works for actions that are started with a button, in automatic push mode and the implementation doesn't seem to handle cancellation. Most of the usage I have for async are not triggered by a button, but when loading certain components.
As for the AsyncManager, I feel it has a better interface. It's outdated but implements a cleaner way to use async computations than Vaadin has out of the box right now.
I'm not saying it shouldn't be further simplified, but there's already a much shorter way in the core APIs as long as you accept that cancelation is triggered only by UI detach but not by detaching the target label:
var future = CompletableFuture.supplyAsync(() -> heavyLoading());
future.thenAccept(UI.getCurrent().accessLater(label::setText, () -> future.cancel(true)));
Without detach handling, there's an even shorter approach with a simple helper method that creates and Executor
around UI::access
:
CompletableFuture.supplyAsync(() -> heavyLoading())
.thenAcceptAsync(wrapper::setText, CurrentUIAccessExecutor.get());
CurrentUIAccessExecutor.get()
can be implemented like this (plus some error handling):
public static Executor get() {
UI ui = UI.getCurrent();
return task -> ui.access(task::run);
}
@Legioth
var future = CompletableFuture.supplyAsync(() -> heavyLoading());
future.thenAccept(UI.getCurrent().accessLater(label::setText, () -> future.cancel(true)));
as long as you accept that cancelation is triggered only by UI detach but not by detaching the target label
That's a big if. So it only triggers when the Browser Tab is closed, and not if the user just navigates somewhere else. Also while it looks simpler at the first glance, it's still a bit of boilerplate code that's just crammed into a single line.
Further It doesn't address the exception handling. So you'd have to add an additional .exceptionally
with a further UI.getCurrent().access(() -> {})
I just feel it's a lot of code to do a simple thing.
As for the executor variant, it's still missing exception handling, and now doesn't cancel the task at all, which I think is a must have if the task is heavy enough to require running in a different thread.
If you care about cancelation, then you should note that your initial example doesn't do anything to stop the computation running in the background thread. CompletableFuture::cancel
has this documentation for the mayInterruptIfRunning
parameter:
this value has no effect in this implementation because interrupts are not used to control processing.
Thanks for the heads-up, I guess a proper implementation would have to use a executor that's interruptable and then inherit from CompletableFuture overwrite cancel
to actually support mayInterruptIfRunning
. Or use FutureTask
with it's own thread.
edit: AsyncManager does the latter: https://github.com/fluorumlabs/async-manager/blob/598fa8addfd8b1ec44693556f12e2e6bbdedbfe8/src/main/java/org/vaadin/flow/helper/AsyncTask.java#L191
For me this just further exemplifies this isn't trivial and a reliable API to do this would be helpful.
Describe your motivation
Currently loading data asynchronously and then displaying said data requires more lines of code than I'd like. Currently the shortest way I can think of looks like this:
With exception handling and session access it gets even more more complicated.
Describe the solution you'd like
Instead I'd prefer something simple such as
Additional context
Here is my current implementation of async tasks: