Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Background tasks: Task and Platform.runLater

All JavaFX handlers run in one thread, the JavaFX Application Thread. The same thread repaints the window. If it reads a big file or waits for a server, the window "freezes" for that time: buttons do not respond, and the system may mark the program as "Not responding". So long work is moved to a background thread, and you come back to the JavaFX thread only with a ready result.

The main rule

WhereWhat is allowed
A background threadReading files, network and database access, calculations
The JavaFX threadChanging nodes and on-screen lists, opening windows

A background thread must not touch the interface. To run code in the JavaFX thread, pass it to Platform.runLater.

A thread and Platform.runLater

Let's load the notes in a virtual thread and hand the result to the interface:

// src/main/java/com/example/notes/MainController.java — first version of loadNotes()
private void loadNotes() {
    statusLabel.setText("Loading...");
    Thread.ofVirtual().start(() -> {
        List<Note> loaded = storage.load();
        Platform.runLater(() -> {
            notes.setAll(loaded);
            notes.addListener((ListChangeListener<Note>) change -> storage.save(notes));
            statusLabel.setText("Notes loaded: " + loaded.size());
        });
    });
}

storage.load() runs in the background, and the lambda inside runLater is queued for the JavaFX thread and runs when that thread is free. The saving listener is attached only after loading; otherwise an empty list could overwrite the file.

A status line

For messages, let's add an indicator and a label to the bottom row:

<!-- src/main/resources/com/example/notes/main-view.fxml — the bottom row -->
<HBox spacing="8" alignment="CENTER_LEFT">
    <padding>
        <Insets top="12"/>
    </padding>
    <Button fx:id="addButton" text="Add" onAction="#addNote"/>
    <Button fx:id="deleteButton" text="Delete" onAction="#deleteNote"
            styleClass="danger"/>
    <ProgressIndicator fx:id="progress" prefWidth="20" prefHeight="20" visible="false"/>
    <Label fx:id="statusLabel"/>
</HBox>

Do not forget <?import javafx.scene.control.ProgressIndicator?> at the top of the file, and the fields in the controller:

// src/main/java/com/example/notes/MainController.java — new fields
@FXML
private ProgressIndicator progress;
@FXML
private Label statusLabel;

Task

Task is the JavaFX class for background work. Its call() method runs in the background, while the onSucceeded and onFailed handlers already run in the JavaFX thread, so no runLater is needed. A task also has running, progress and message properties that are handy to bind the interface to. In initialize(), the old two loading lines are replaced by a call to loadNotes():

// src/main/java/com/example/notes/MainController.java — loadNotes() with a Task
private void loadNotes() {
    Task<List<Note>> task = new Task<>() {
        @Override
        protected List<Note> call() {
            return storage.load();
        }
    };
    task.setOnSucceeded(event -> {
        notes.setAll(task.getValue());
        notes.addListener((ListChangeListener<Note>) change -> storage.save(notes));
        statusLabel.setText("Notes loaded: " + notes.size());
    });
    task.setOnFailed(event -> {
        statusLabel.setText("Could not read the notes file");
        task.getException().printStackTrace();
    });
    progress.visibleProperty().bind(task.runningProperty());
    statusLabel.setText("Loading...");
 
    Thread thread = new Thread(task);
    thread.setDaemon(true);
    thread.start();
}

task.getValue() is what call() returned. The indicator is visible while the task runs. The thread is marked as a daemon (setDaemon(true)) so that an unfinished task does not keep the program alive after the window closes.

Trying it out

Run the application. For a split second the bottom shows "Loading..." with a spinning indicator, then "Notes loaded: 2". Now break notes.json by adding an extra comma and start again: the window opens with an empty table and "Could not read the notes file", and the Jackson error text is in the terminal. The saving listener is not attached, so the damaged file is not overwritten with an empty list.

Common mistake

The interface is changed right from a background thread. Sometimes this "works" without errors but leads to random drawing glitches, and some JavaFX operations check the thread and fail at once. For example, new Alert(...).show() inside new Thread(...) gives java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4. Do everything that touches nodes, on-screen lists and windows in onSucceeded, onFailed or inside Platform.runLater.

Thanks for reading!

Donate to the site — we work hard for you.