Issue
Currently I am making a program that reminds me when to water my plants, while also putting the weather into account. I would like to display the current temperature and humidity, and I have made code that does that well enough already. However, this code only works when manually running the method via a button press, and throws Exception in thread "pool-3-thread-1" java.lang.IllegalStateException: Not on FX application thread; currentThread = pool-3-thread-1
when I attempt to run it in a ScheduledExecutorService. From my understanding JavaFX does not allow other threads to edit JavaFX components without Platform.runLater, however I can't seem to find anything about Platform.runLater being combined with ScheduledExecutorService.
Here is my update method:
public void update() {
final Runnable updater = new Runnable() {
public void run() {
humidityLabel.setText("Humidity: " + Double.toString(Weather.getHumidity()) + "%");
humidityDialArm.setRotate(Weather.getHumidity() * 1.8);
tempLabel.setText("Temperature: " + Double.toString(Weather.getTemperature()) + "°F");
temperatureDialArm.setRotate(Weather.getTemperature()*1.5);
icon = Weather.getIcon();
conditionLabel.setText(Weather.getCondition());
}
};
final ScheduledFuture<?> updaterHandle = scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
}
And here is my main method:
public static void main(String[] args) {
App app = new App();
launch();
app.update();
}
I found a similar problem here, however I haven't been able to find a way to get Platform.runLater to work well with the ScheduledExecutorService. I also found this on GitHub, however I can't tell what the fix for this problem was other than it was fixable. I also tried putting a while loop at main that would just constantly update it, but that just caused the program to hang and eventually crash. Even if it did work, that would also make it not runnable for long periods of time as the API I am using limits the amount of GET requests per day.
Solution
Use ScheduledService
The javafx.concurrent.ScheduledService
class provides a way to repeatedly do an action and easily communicate with the FX thread. Here is an example:
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
public class WeatherService extends ScheduledService<WeatherService.Result> {
@Override protected Task<Result> createTask() {
return new Task<>() {
@Override protected Result call() throws Exception {
// this is invoked on the background thread
return new Result(
Weather.getTemperature(),
Weather.getHumidity(),
Weather.getCondition(),
Weather.getIcon()
);
}
};
}
public record Result(double temperature, double humidity, String condition, Image icon) {}
}
Then where you use the service you'd add an on-succeeded handler to handle updating the UI:
service.setOnSucceeded(e -> {
var result = service.getValue();
// update UI (this handler is invoked on the UI thread)
});
To have it execute every 10 minutes, with an initial delay of 10 minutes, to match what you're doing with the ScheduledExecutorService
, you would do:
service.setDelay(javafx.util.Duration.minutes(10));
service.setPeriod(javafx.util.Duration.minutes(10));
// you can define the thread pool used with 'service.setExecutor(theExecutor)'
When first configuring the service. You also need to maintain a strong reference to the service; if it gets garbage collected, then the task will not be rescheduled.
Use Platform#runLater(Runnable)
If you have to use ScheduledExecutorService
for some reason, then you should run the code that updates the UI in a runLater
call. Here's an example:
public void update() {
final Runnable updater =
() -> {
// get information on background thread
double humidity = Weather.getHumidity();
double temperature = Weather.getTemperature();
Image icon = Weather.getIcon();
String condition = Weather.getCondition();
// update UI on FX thread
Platform.runLater(
() -> {
humidityLabel.setText("Humidity: " + humidity + "%");
humidityDialArm.setRotate(humidity * 1.8);
tempLabel.setText("Temperature: " + temperature + "°F");
temperatureDialArm.setRotate(temperature * 1.5);
iconView.setImage(icon);
conditionLabel.setText(condition);
});
};
scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
}
Answered By - Slaw
Answer Checked By - Pedro (JavaFixing Volunteer)