App hangs up or "Not on FX application thread" occurs during app activity

The application reacts on actions which occur on gamepad. When button is pressed something happens on UI. But I ran at the issue with app hangs up or "java.lang.IllegalStateException: Not on FX application thread" exception.

In order to fix it I tried the following approaches: Platform.runLater() and Task usage. But it didn't help.

Here is the problem code:

public class GamepadUI extends Application{

    private static final int WIDTH = 300;
    private static final int HEIGHT = 213;

    private Pane root = new Pane();
    private ImageView iv1 = new ImageView();

    private boolean isXPressed = false;

    @Override
    public void start(Stage stage) throws Exception {
        initGUI(root);

        Scene scene = new Scene(root, WIDTH, HEIGHT);
        stage.setScene(scene);
        stage.setResizable(false);
        stage.show();
    }

    public void pressBtn() {
        if(!isXPressed) {
            iv1.setVisible(true);
            isXPressed = true;
        }
    }

    public void releaseBtn() {
        if(isXPressed) {
            iv1.setVisible(false);
            isXPressed = false;
        }
    }

    private void initGUI(final Pane root) {
        Image image = new Image(Props.BUTTON);
        iv1.setImage(image);
        iv1.setLayoutX(198);
        iv1.setLayoutY(48);
        iv1.setVisible(false);
        root.getChildren().add(iv1);

        runTask();
    }

    public void runTask() {
        Task task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                initStubGamepad();
                return null;
            }
        };

        new Thread(task).start();
    }

    public static void main(String[] args) {
        launch(args);
    }

    public void initStubGamepad() {
        Random rnd = new Random();
        try {
            while (true) {
                if (rnd.nextInt(30) == 3) {
                    pressBtn();
                } else if (rnd.nextInt(30) == 7) {
                    releaseBtn();
                }
            }
        } catch (Exception ex) {
            System.out.println("Exception: " + ex);
        }
    }
}

initStubGamepad() emulates gamepad buttons activity polling. When user presses any button (rnd.nextInt(30) == 3) - an image appears on the UI. When user releases that button (rnd.nextInt(30) == 7) - an image disappears from the UI.

In case above java.lang.IllegalStateException: Not on FX application thread occurs. If you change runTask() to something like this:

Platform.runLater(new Runnable() {
    @Override
    public void run() {
        initStubGamepad();
    }
});

Then app will hang or even main UI won't appear at all, but gamepad activity continues.

What I want is just to show/hide different images when some activity is detected on gamepad (btw, there's no way to monitor gamepad activity except for gamepad polling in an infinite loop). What did I wrong


Solution 1:

Explanation

In the first scenario, when you are using

Task task = new Task<Void>() {
  @Override
  protected Void call() throws Exception {
      initStubGamepad();
      return null;
  }
}

Inside initStubGamepad(), which is running on a Task, you are trying to update the UI components inside pressBtn() and releaseBtn() methods, which is why you are facing a

java.lang.IllegalStateException: Not on FX application thread

because all the UI updates must occur on the JavaFX thread

In the second scenario, when you are using

Platform.runLater(new Runnable() {
    @Override
    public void run() {
        initStubGamepad();
    }
});

the UI doesnt appear, because you have an infinite loop inside the initStubGamepad(), which puts the JavaFX application thread run on an infinite loop

Solution

By the time you have reach here, you must have already found the solution. In case you haven't, try try to put the update the Javafx components on the UI thread. So, instead of calling initStubGamepad() inside Platform.runLater, try calling pressBtn() and releaseBtn() inside it.

Try using

while (true) {
   if (rnd.nextInt(30) == 3) {
      Platform.runLater(() -> pressBtn());            
   } else if (rnd.nextInt(30) == 7) {
       Platform.runLater(() -> releaseBtn());            
   }
}

or you may also use

public void pressBtn() {
    if(!isXPressed) {
        Platform.runLater(() -> iv1.setVisible(true));
        isXPressed = true;
    }
}