Search code examples
javamultithreadingjavafxrunnable

How does Platform.runLater() function?


I have a simple app which updates the data in the background and while it updates, it disables all the other buttons and enables a TextArea to show the progress.

Steps:

  1. Disable all the other buttons in the mainUI (Button name: plotButton)

  2. Enable a TextArea showing that the updating has started (TextArea name: infoLogTextArea)

  3. Then only start the update method (update() throws Exceptions).

Here is the code:

@FXML
    public void handleUpdateButton() {
        
        infoLogTextArea.setVisible(true);
        infoLogTextArea.appendText("Please wait while downloading data from internet.....\n");      
        plotButton.setDisable(true);
        updateButton.setDisable(true);
            
        if(c!=null) {
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    // Thread.sleep(10000); -> sleep for 10secs
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                c.updateData();
                                infoLogTextArea.appendText(c.getErrorLog().toString());
                                plotLabel.setText(c.getCityData().size()+" cities found and updated from internet");
                                infoLogTextArea.appendText("Successfully updated the data from Internet\n");
                            }catch (IOException e) {
                                infoLogTextArea.setText("Couldnot update the data from web: "+e.getMessage()+"\n");
                            }
                            finally {
                                plotButton.setDisable(false);
                                updateButton.setDisable(false);
                            }
                        }
                    });
                }
            };
            
            new Thread(task).start();
            
        }else {
            System.out.println("c not initialized");
        }
    }

Now the code works well but sometimes steps 1 and 2 are not executed and it starts step 3 (updating) which can freeze the program. If I put Thread.sleep(10 secs) in between step 2 and 3 then it works completely fine. (it is commented in the code)

But can anybody explain what is going on behind the scenes and why Platform.runLater() doesn't work all the time?


Solution

  • A JavaFX application runs on the Application thread, which handles all the UI elements. This means that if you click Button A and clicking that button starts method A that takes 5 seconds to complete, and then one second after clicking that button, you try to click Button B which starts method B, method B won't start until method A finishes. Or possibly Button B won't even work until method A finishes, I'm a little fuzzy on the detail there.

    A good way to stop your application from freezing is to use Threads. To fix the above scenario, clicking Button A will start method A that starts a new Thread. Then the Thread can take as long as it wants to complete without locking up the UI and preventing you from clicking Button B.

    Now, say something in method A needed to be on the application thread, for example, it updated a UI component, like a Label or a TextField. Then inside your Thread in Method A you would need to put the part that affects the UI into a Platform.runLater(), so that it will run on the Application Thread with the rest of the UI.

    What this means for your example is that you have two options.
    1. Don't use threads at all, since you don't want the user to be interacting with the UI while the updates are happening anyway.
    2. move c.updateData() out of the Platform.runLater() like this:

     Runnable task = new Runnable() {
                @Override
                public void run() {
                    c.updateData();
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                infoLogTextArea.appendText(c.getErrorLog().toString());
                                plotLabel.setText(c.getCityData().size()+" cities found and updated from internet");
                                infoLogTextArea.appendText("Successfully updated the data from Internet\n");
                            }catch (IOException e) {
                                infoLogTextArea.setText("Couldnot update the data from web: "+e.getMessage()+"\n");
                            }
                            finally {
                                plotButton.setDisable(false);
                                updateButton.setDisable(false);
                            }
                        }
                    });
                }
            };
    

    Either one of those will work, but what you're doing right now is you're on the application thread, and then you start another thread whose only purpose is to run something on the application thread.