1

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.

N3ther
  • 76
  • 1
  • 10
  • Assuming the `launch()` call is `Application#launch(String...)`, then note that the method will not return until the JavaFX application exits, meaning the `app.update()` call will be invoked too late. Also, assuming `App` extends `Application`, you should not be creating your own instance. An instance is created for you by the `launch()` call, and it's that instance that's managed by the platform. – Slaw May 15 '22 at 02:05

1 Answers1

4

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);
}
Slaw
  • 37,820
  • 8
  • 53
  • 80