I have a JavaFX application where one functionality involves searching (large) binary data for specific patterns. To keep the GUI responsive, I am creating a concurrent task and put it on a differen thread:
public void search(SearchPattern pattern) {
Task<ObservableList<...>> task = new Task<>() {
@Override protected ObservableList<...> call() throws Exception {
try {
for(int i=0;i<....size();i++) {
if (isCancelled()) {
break;
}
if(i%50000 == 0) {
updateProgress(i, entries.size());
}
if(pattern.matches(entries.get(i))) {
// do stuff
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// IO cleanup
}
return FXCollections.observableArrayList(...);
}
};
progressBar.progressProperty().bind(task.progressProperty());
task.setOnSucceeded(e -> {
unregisterRunningTask(task);
searchResults = task.getValue();
});
Thread thread = new Thread(task);
registerRunningTask(task);
thread.setDaemon(false);
thread.start();
}
This works fine, however mingles (library) functionality with GUI code. In particular I would like to put the search code into a separate library, write test-cases for that functionality - completely automated, without a GUI - and of course when testing from the commandline, a separate thread is not needed. Right now, I am limited to putting pattern.matches(entries.get(i))
in that library. A cleaner way would be to have a function
searchpattern(filename) {
for(int=0;i< ... ;i++) {
...
}
return observableArrayList(...);
}
in the library that includes the complete loop, write my test-cases and benchmarks on that, and then somehow involve that function in the GUI code. The problem I face when separating this code is:
updateProgress()
requires a reference to a task. Of course I could always create a task when trying to implement test-cases, i.e. even when invoking searchpattern(filename)
from a non-GUI context. I could simply omit any progress updates in my code, but that creates a very bad user-experience.
Is there any other way to pass updates from that (potentially) long-running function using some abstraction layer, that makes this function easy in a library both from a (threaded) GUI context and a non-threaded commandline context?
Here are two approaches. To make things more concrete, suppose we have a class that implements a potentially long-running process on a list:
public abstract class ListProcessor<T> {
private final List<T> items ;
public ListProcessor(List<T> items) {
this.items = items ;
}
public void process() {
for (int i = 0 ; i < items.size(); i++) {
processItem(items.get(i));
}
}
public abstract processItem(T item);
}
(I'm making a ton of assumptions here about the list not being modified during processing, etc.)
The first approach is as described by @Slaw in a comment. In this approach, your external library class keeps a list of listeners that are notified when the number of items processed changes. You could simply use a DoubleConsumer
for this. For example:
public abstract class ListProcessor<T> {
private final List<T> items ;
private final List<DoubleConsumer> listeners = new ArrayList<>();
public ListProcessor(List<T> items) {
this.items = items ;
}
public void addListener(DoubleConsumer listener) {
listeners.add(listener);
}
public void removeListener(DoubleConsumer listener) {
listeners.remove(listener);
}
public void process() {
int n = items.size();
for (int i = 0 ; i < n; i++) {
processItem(items.get(i));
listeners.forEach(l -> l.accept(1.0 * i / n));
}
}
public abstract processItem(T item);
}
You can use this in a task as follows:
List<Widget> widgets = ... ;
ListProcessor<Widget> widgetProcessor = new WidgetProcessor(widgets);
Task<Void> widgetTask = new Task<>() {
@Override
protected Void call() {
DoubleConsumer progressUpdater = prog -> updateProgress(prog, 1.0);
widgetProcessor.addListener(progressUpdater);
widgetProcessor.process();
widgetProcessor.removeListener(progressUpdater);
updateProgress(1.0, 1.0);
return null;
}
};
progressBar.progressProperty().bind(widgetTask.progressProperty());
// ...
executor.execute(widgetTask);
This relies on your library class implementing a listener-notification mechanism. If you don't want to do that, or if you are using some third-party library that doesn't implement it, you can poll the processing object for its current progress. I would do the polling from an AnimationTimer
, though other approaches are possible. The minimum requirement (as far as I can tell) is that the processing object has a way of reporting its progress in a thread-safe manner (as you will be polling it from a different thread to the one that is updating it).
So, for example:
public abstract class ListProcessor<T> {
private final List<T> items ;
private volatile double progress = 0.0;
public ListProcessor(List<T> items) {
this.items = items ;
}
public void process() {
int n = items.size();
for (int i = 0 ; i < n; i++) {
processItem(items.get(i));
progress = (1.0 * i / n);
}
}
public double getProgress() {
return progress;
}
public abstract processItem(T item);
}
And then
List<Widget> widgets = ... ;
ListProcessor<Widget> widgetProcessor = new WidgetProcessor(widgets);
Task<Void> widgetTask = new Task<>() {
@Override
protected Void call() {
AnimationTimer progressUpdater = new AnimationTimer() {
@Override
public void handle(long now) {
updateProgress(widgetProcessor.getProgress(), 1.0);
}
};
progressUpdater.start();
widgetProcessor.process();
progressUpdater.stop();
updateProgress(1.0, 1.0);
return null;
}
};
progressBar.progressProperty().bind(widgetTask.progressProperty());
// ...
executor.execute(widgetTask);
In both cases, you should make sure you implement proper clean-up in the case of exceptions being thrown, etc. (It's probably better to do the clean-up in a state listener or in the succeeded()
and failed()
methods.)