Reputation: 9761
I'm pretty confused about the concept of Task
/Service
in JavaFX.
I have used a model based on a background thread for my background work, which call Platform.runLater
for any update to the UI.
Let's say I'm not interested in a progress bar or such. I'm doing some real work on my model that must be updated in the view of the GUI (e.g a list of participants which updates over time based on some connection in the background, list of participant based on some user input, classified by age and origin). This is what I usually achieve with background threads that I start, and within which I use Platform.runLater
.
Now in JavaFX 2 they have all this concurrency using Task
s and Service
s, suggesting that it is better to use them. But I don't see any examples that achieve what I'm talking about.
Updating the progress bar by binding some properties is nice (but those are information on the task not your model).
So, how can I actually update the content of my views based on my model? Should I call Platform.runLater
from within the Task
? If not, what is the mechanism? How do I catch when the tasks have succeed and get the result (the update of the actual model) to update the view?
The tutorials by Oracle unfortunately were not very good in this regard. Pointing me to some good tutorials would also help.
Upvotes: 6
Views: 9027
Reputation: 209418
The Task
and Service
classes are designed to encourage good practice and proper use of concurrency for some (but not all) common scenarios in GUI programming.
A typical scenario is that the application needs to execute some logic in response to a user action which may take a long time (maybe a long calculation, or, more commonly, a database lookup). The process will return a result which is then used to update the UI. As you know, the long-running process needs to be executed on a background thread to keep the UI responsive, and the update to the UI must be executed on the FX Application Thread.
The Task
class provides an abstraction for this kind of functionality, and represents a "one-off" task that is executed and produces a result. The call()
method will be executed on the background thread, and is designed to return the result of the process, and there are event listeners for when the task completes that are notified on the FX Application thread. The developer is strongly encouraged to initialize the Task
implementation with immutable state and have the call()
method return an immutable object, which guarantees proper synchronization between the background thread and the FX Application Thread.
There are additional common requirements on these kinds of tasks, such as updating a message or the progress as the task progresses. The application may also need to monitor the life-cycle state of the class (waiting to run, running, completed, failed with an exception, etc). Programming this correctly is quite subtly difficult, as it necessarily involves accessing mutable state in two different threads, and there are many application developers who are unaware of the subtleties. The Task
class provides simple hooks for this kind of functionality and takes care of all the synchronization.
To use this functionality, just create a Task
whose call()
method returns the result of your computation, register a handler for when the state transitions from RUNNING
to SUCCEEDED
, and run the task in a background thread:
final Task<MyDataType> task = new Task<MyDataType>() {
@Override
public MyDataType call() throws Exception {
// do work here...
return result ;
}
};
task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
MyDataType result = task.getValue(); // result of computation
// update UI with result
}
});
Thread t = new Thread(task);
t.setDaemon(true); // thread will not prevent application shutdown
t.start();
The way this works behind the scenes is that the Task
maintains a state
property, which is implemented using a regular JavaFX ObjectProperty
. The Task
itself is wrapped in a private implementation of Callable
, and the Callable
implementation is the object passed to the superclass constructor. Consequently, the Callable
's call()
method is actually the method executed in the background thread. The Callable
's call()
method is implemented as follows:
Platform.runLater()
) that updates the state
, first to SCHEDULED
, then to RUNNING
call()
method of the Task
(i.e. the user-developed call()
method)value
property to the result of the call()
methodstate
property to SUCCEEDED
This last step will of course invoke listeners registered with the state
property, and since the state change was invoked on the FX Application Thread, so to will those listeners' handle()
methods.
For a full understanding of how this works, see the source code.
Commonly, the application may want to execute these tasks multiple discrete times, and monitor the current state representing all of the processes (i.e. "running" now means one instance is running, etc). The Service
class simply provides a wrapper for this via a createTask()
method. When the Service
is started, it gets a Task
instance by calling createTask()
, executes it via its Executor
, and transitions its own state accordingly.
There are of course many concurrency use cases that don't fit (at least cleanly) into the Task
or Service
implementations. If you have a single background Thread
that is running for the entire duration of your application (so it represents a continuous process, rather than a one-off task), then the Task
class is not a good fit. Examples of this might include a game loop, or (perhaps) polling. In these cases you may well be better off using your own Thread
with Platform.runLater()
to update the UI, but of course you have to handle proper synchronization of any variables that may be accessed by both threads. In my experience, it is worth spending some time thinking about whether these requirements can be re-organized into something that does fit into the Task
or Service
model, as if this can be done the resulting code structure is often much cleaner and easier to manage. There are certainly cases where this is not the case, however, in which case using a Thread
and Platform.runLater()
is appropriate.
One last comment on polling (or any other requirement for a periodically-scheduled background task). The Service
class looks like a good candidate for this, but it turns out to be quite hard to manage the periodicity effectively. JavaFX 8 introduced a ScheduledService
class which takes care of this functionality quite nicely, and also adds handling for cases such as repeated failure of the background task.
Upvotes: 21