javamultithreadingjavafxjavafx-2task

JavaFX multithreading - joining threads won't update the UI


I am trying to make a loader dialog where the user can know that the program is loading what was requested and that the program is running as expected.

But as so, I need to join() the parser thread and before keep going on the main and this makes the dialog blank.

ParserTask.java

public class ParserTask extends Task<Void>{
    @Override
    protected Void call() throws Exception {
        for(int i=0;i<10;i++){
            //parse stuff
            Thread.sleep(1000);
            updateProgress(i,9);
            updateMessage("foo no:"+ i);
        }
        updateMessage("done");
        return null;
    }

}

Main.java

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main  extends Application {

    @Override
    public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 300, 150);
        stage.setScene(scene);
        stage.setTitle("Progress Controls");
        Button btn = new Button("call the thread");
        btn.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                threadDialog();
            }
        });
        VBox vb = new VBox();
        vb.getChildren().addAll(btn);
        scene.setRoot(vb);
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
    private void threadDialog() {
        Stage stageDiag = new Stage();
        ParserTask pTask = new ParserTask();
        final Label loadingLabel = new Label("");
        loadingLabel.textProperty().bind(pTask.messageProperty());
        final ProgressBar pbar = new ProgressBar();
        pbar.setProgress(0);
        pbar.setPrefHeight(20);
        pbar.setPrefWidth(450);
        pbar.progressProperty().bind(pTask.progressProperty());
        GridPane grid = new GridPane();
        grid.setHgap(14.0);
        grid.add(loadingLabel, 0, 0);
        grid.add(pbar, 1, 1);
        Scene sceneDiag = new Scene(grid);
        stageDiag.setScene(sceneDiag);
        stageDiag.setTitle("Foo thread is loading");
        stageDiag.show();
        Thread parser = new Thread(pTask);
        parser.start();
        try {
            parser.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

If, I comment/ remove this code:

        try {
            parser.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

Then, I can see the thread loading as expected, otherwise, I can only see a blank screen that will be in the final state (the last message and the full progress bar).

    while(!loadingLabel.getText().contains("one")){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

And even while(parser.isAlive()); without success.

My question is: How can I wait for my thread and keep the UI working?


Solution

  • You should never block the UI thread.

    Typical pattern for background processing looks like follows:

    1. Change UI to indicate that background processing is in progress (show progress bar, disable buttons, etc)
    2. Start a background thread
    3. In the background thread, after processing has finished, change the UI back and display processing results. Code that does it should be invoked using Platform.runLater() (as well as any other code that interacts with the UI from background threads)

    Simplified example:

    btn.setOnAction(new EventHandler<ActionEvent>(){
        @Override
        public void handle(ActionEvent arg0) {
            // 1
            setBackgroundProcessing(true); 
    
            // 2
            new Thread() {
                public void run() {
                    doProcessing();
    
                    // 3
                    Platform.runLater(new Runnable() {
                        public void run() {
                            displayResults();
                            setBackgroundProcessing(false);
                        }
                    });
                }
            }.start();
        }
    });
    

    The code above is not very elegant, you may want to use thread pools and/or ListenableFutures to make it look better.