Propper JavaFX thread implementation

2019-07-22 20:01发布

问题:

In my GUI I have a TableView that should show a list of loaded files, after a class called PathGetter has finished loading them into an ObservableArrayList, but I just can't implement the task correctly.

This is the important part in the JavaFX class

browseButton.setOnAction(event -> {
            File dir = folderPicker.showDialog(bwindow);
            if(dir != null){
                directoryLocation.setText(String.valueOf(dir));
                bottom.getChildren().add(new javafx.scene.control.Label("Loading Tracks"));
                //PathGetter.getPath(directoryLocation.getText());
                PathGetter task = new PathGetter(directoryLocation.getText());
                Thread th = new Thread(task);
                try {
                    pjesme = FXCollections.observableArrayList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                selection.setItems(pjesme);
                chSelAll.setDisable(false);
                chSelIncomplete.setDisable(false);
                chSelNoCover.setDisable(false);
            }

        });

And this is the class that should work in

public class PathGetter extends Task<ObservableList<Track>> {

    static boolean getSubDirs;
    static ArrayList <Track> allFiles;
    public static int trNr = 0;
    private static String fullPath;

    public PathGetter(String path) {
        fullPath = path;
    }

    public static int getTrNr() {
        return trNr;
    }

    public static void setTrNr(int trNr) {
        PathGetter.trNr = trNr;
    }

    public static boolean isSupported (File f){
        //supported file types
        if(String.valueOf(f).endsWith(".flac") || String.valueOf(f).endsWith(".mp3") || String.valueOf(f).endsWith(".aiff") || String.valueOf(f).endsWith(".ogg") || String.valueOf(f).endsWith(".mp4")){
            return true;
        }else{
            return false;
        }
    }

    @Override
    protected ObservableList<Track> call() throws Exception {
        getSubDirs = Browser.chSubDirs.isSelected();
        allFiles = new ArrayList<Track>();
        Queue<File> dirs = new LinkedList<File>();
        dirs.add(new File(fullPath));
        while (!dirs.isEmpty()) {
            for (File f : dirs.poll().listFiles()) {
                if (f.isDirectory() && getSubDirs == true) {
                    dirs.add(f);
                } else if (f.isFile() && isSupported(f)) {
                    allFiles.add(new Track(f));
                    setTrNr(getTrNr()+1);
                }
            }
        }
        ObservableList<Track> returnList = FXCollections.observableArrayList(allFiles);
        return returnList;
    }
}

I don't understand how to make the TableView wait for the task to be finished, without blocking the entire JavaFX thread, which basically defeats the purpose of a task. I want it to be able to show progress in real time, simply by displaying the number of added tracks at that moment.

回答1:

There are two threading rules specific to JavaFX:

  1. Any changes to the UI must be made on the FX Application Thread. Not doing this will either cause IllegalStateExceptions to be thrown at runtime, or may put the UI in an inconsistent state, potentially causing unpredicatable behavior at arbitrary points in the future.
  2. Any code that takes a long time to run should be performed on a background thread. Not doing so will cause the UI to become unresponsive while that code is running.

Additionally, there is a general rule about threading:

Care must be taken when accessing mutable state in multiple threads. In particular, operations in a given thread should be ensured to be atomic, and special care may need to be taken to ensure changes to the state of the data made in one thread are visible to another thread.

Getting this last part correct is particularly challenging. Additionally, when using a background thread in a UI environment, you almost always want to share the results of the process, and sometimes data that is computed during the process, between the background thread and the UI thread. Because this is challenging, JavaFX provides a Task class (and some other related classes) that take care of the trickier parts of this in order to cover most use cases.

In particular, the Task class exposes various properties (including state, progress, message, and value), along with thread-safe updateXXX methods. The update methods are safe to be called from any thread, and will both ensure the properties are updated on the UI thread, and throttle the number of updates to them (coalescing updates together essentially if they occur within the time the UI is updated). This means it is safe to call the update methods from the background thread, and observe the properties in the UI thread. Additionally, you can call these methods as often as you like without "flooding" the UI thread and causing it to become unresponsive that way.

The Task class also exposes handlers for transitioning from one state to another, such as setOnSucceeded (invoked when the task completes normally) and setOnFailed (invoked when the task throws an exception). These are also handled on the FX Application Thread.

Your task subclass can:

  1. Use the message property to update the number of tracks processed
  2. Return the list of tracks generated

From the UI code, you can bind the text of a label to the message property. You can also use on onSucceeded handler to update the UI when the task completes.

To ensure you don't share mutable state between threads, other than that which is properly managed by the Task machinery, you should properly encapsulate your class. This means not exposing any state that is manipulated by the task itself. None of your state should be static (and there is no obvious reason you would want to do this anyway).

So I would write the task as follows:

public class PathGetter extends Task<ObservableList<Track>> {

    private final boolean getSubDirs;
    private final String fullPath;

    public PathGetter(String path, boolean getSubDirs) {
        fullPath = path;
        this.getSubDirs = getSubDirs ;
    }

    public static boolean isSupported (File f){
        String fileName = f.toString();
        //supported file types
        return fileName.endsWith(".flac") 
               || fileName.endsWith(".mp3")  
               || fileName.endsWith(".aiff") 
               || fileName.endsWith(".ogg") 
               || fileName.endsWith(".mp4") ;
    }

    @Override
    protected ObservableList<Track> call() throws Exception {

        List<Track> allFiles = new ArrayList<Track>();
        Queue<File> dirs = new LinkedList<File>();
        dirs.add(new File(fullPath));
        while (!dirs.isEmpty()) {
            for (File f : dirs.poll().listFiles()) {
                if (f.isDirectory() && getSubDirs) {
                    dirs.add(f);
                } else if (f.isFile() && isSupported(f)) {
                    allFiles.add(new Track(f));
                    updateMessage("Number of tracks processed: "+allFiles.size());
                }
            }
        }
        ObservableList<Track> returnList = FXCollections.observableArrayList(allFiles);
        return returnList;
    }
}

Now from the UI you can do something like:

browseButton.setOnAction(event -> {
    File dir = folderPicker.showDialog(bwindow);
    if(dir != null){
        directoryLocation.setText(String.valueOf(dir));
        Label label = new Label("Loading Tracks");
        bottom.getChildren().add(label);

        PathGetter task = new PathGetter(directoryLocation.getText(), Browser.chSubDirs.isSelected());
        Thread th = new Thread(task);

        // keep label showing message from task:
        label.textProperty().bind(task.messageProperty());

        task.setOnSucceeded(e -> {
            selection.setItems(task.getValue());
            chSelAll.setDisable(false);
            chSelIncomplete.setDisable(false);
            chSelNoCover.setDisable(false);
        });

        task.setOnFailed(e -> {
            // handle exception ...

            // and log it
            task.getException().printStackTrace();
        });

        chSelAll.setDisable(true);
        chSelIncomplete.setDisable(true);
        chSelNoCover.setDisable(true);

        // make sure thread doesn't prevent application exit:
        th.setDaemon(true);

        // set it going:
        th.start();

    }

});


回答2:

You could add a listener to your ObservableArrayList.

         pjesme.addListener(new ListChangeListener<Track>() {
            @Override
        public void onChanged(ListChangeListener.Change<? extends Track> c) {
            /* Do your Stuff in the gui. Like having the status bar or things */
        }
         });

After you have added the listener you just hand it to the constructor

       public PathGetter(String path, ObservableList<Track> pjesme) {
          fullPath = path;
          this.pjesme = pjesme;
       }

and alter the ObersvableList in your PathGetter class. So everytime you add something to your List the eventlistener will get invoked and you get the chance to update stuff in your GUI.