Keep the UI responsive during slow work

IA tooling, not an exam topic. Threading is an IA-depth technique, not examined material. These are JavaFX patterns to adapt for your IA, and your IA must be your own work.

Some work takes time: saving to a database, calling a network service, or a heavy computation. If you do that work in a button handler, the whole window freezes until it finishes. This page shows why that happens, the raw way to fix it, and the recommended way with Task.

New here? Read Start here: how every recipe fits together first; it explains the two places you edit and the onAction link every recipe relies on.

The one rule that governs this whole page: never touch a UI control from a background thread. Reading or changing a control (a label, a table, a progress bar) must happen on the JavaFX UI thread. Each recipe below shows the safe way to get back onto it.

To keep the examples runnable with no setup, they use a simulated slow operation: a short Thread.sleep. Swap that for your real work (for example the SQLite insert from Step up to a database; MySQL is the same JDBC shape) once you can see the responsive-versus-frozen difference.

// Stand-in for real slow work (a database save, a network call, a heavy loop).
private void slowSave() {
    try {
        Thread.sleep(3000);   // pretend this takes 3 seconds
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

See why slow work freezes the window

What this does: shows the problem. Running slow work directly in a handler blocks the UI thread, so the window stops responding (buttons do nothing, nothing redraws) until the work finishes.

When you would use it in your IA: you do not write this on purpose; you write it by accident the first time you put a real save or network call in a handler. This recipe is here so you recognise the freeze and know it is fixable.

In SceneBuilder (the layout): add a Button and set its onAction to handleSlowSave.

<Button text="Save (freezes)" onAction="#handleSlowSave" />

In the controller (the .java file):

import javafx.fxml.FXML;

@FXML
private void handleSlowSave() {
    // Runs on the UI thread: the window is frozen until slowSave() returns.
    slowSave();
}

Where your own logic goes: nowhere good. This recipe exists to show the wrong place to do slow work. The fix is the next two recipes, where // your IA logic here moves onto a background thread.

Make it your own:

  • Try it: add a second button that just shows an alert, then click Save and immediately click the other button. Nothing happens until the three seconds are up. That frozen window is the problem.

Watch out for:

  • The method named in onAction (handleSlowSave) must exist in the controller, or the screen will not load.
  • A frozen window is not a crash. It is the UI thread being too busy to respond. The next recipes free it up.

Mix with: Run slow work on a background thread, the raw way, Show progress toward a goal.


Run slow work on a background thread, the raw way

What this does: moves the slow work onto a new background thread so the window stays responsive, then hands the UI update back to the UI thread with Platform.runLater.

When you would use it in your IA: any slow operation where you want to understand what is happening under the hood. It is worth seeing once before you switch to the cleaner Task version below.

In SceneBuilder (the layout): add a Button (onAction handleSave) and a Label to show status (fx:id lblStatus).

<Button text="Save" onAction="#handleSave" />
<Label fx:id="lblStatus" />

In the controller (the .java file):

Imports:

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

Controller:

@FXML private Label lblStatus;

@FXML
private void handleSave() {
    lblStatus.setText("Saving...");          // on the UI thread: fine

    new Thread(() -> {
        // This block runs on a background thread, so the window stays responsive.
        // your IA logic here: the slow operation
        slowSave();

        // Never touch a control directly from here. Hand the update back:
        Platform.runLater(() -> lblStatus.setText("Saved"));
    }).start();
}

Where your own logic goes: inside the background block, on the line marked // your IA logic here. The thread and the Platform.runLater hand-back are reusable wiring.

Make it your own:

  • Replace slowSave() with your real slow work (a database insert, a download, a long calculation).
  • Anything that changes a control after the work, put inside the Platform.runLater(...) call.

Watch out for:

  • The fx:id lblStatus must match the @FXML field name, and handleSave must exist in the controller, or the screen will not load.
  • Never update a control directly from the background thread. lblStatus.setText(...) outside Platform.runLater can corrupt the UI or throw. This is the one rule from the top of the page.
  • The () -> { ... } parts are lambdas, which are outside the exam-style Java subset but are the normal way to start a thread and to post a UI update in JavaFX.

Mix with: Run slow work with a Task, the recommended way, Tell the user something.


What this does: uses javafx.concurrent.Task, JavaFX’s built-in tool for background work. Its call() method runs on a background thread, and setOnSucceeded and setOnFailed run back on the UI thread, so updates and error handling are clean without writing Platform.runLater yourself.

When you would use it in your IA: this is the one to reach for in a real project. It handles the thread, success, and failure in a tidy shape, and it is what an examiner expects to see for responsive slow work.

In SceneBuilder (the layout): add a Button (onAction handleSave) and a Label (fx:id lblStatus).

<Button text="Save" onAction="#handleSave" />
<Label fx:id="lblStatus" />

In the controller (the .java file):

Imports:

import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

Controller:

@FXML private Label lblStatus;

@FXML
private void handleSave() {
    Task<Void> saveTask = new Task<>() {
        @Override
        protected Void call() throws Exception {
            // Runs on a background thread. Do NOT touch controls here.
            // your IA logic here: the slow operation (e.g. a SQLite insert)
            slowSave();
            return null;
        }
    };

    // These run back on the UI thread, so updating controls here is safe.
    saveTask.setOnSucceeded(e -> lblStatus.setText("Saved"));
    saveTask.setOnFailed(e -> lblStatus.setText("Save failed"));

    lblStatus.setText("Saving...");
    new Thread(saveTask).start();
}

Where your own logic goes: inside call(), on the line marked // your IA logic here. That is your slow operation, for example the JDBC insert from the database recipe. The Task, its success and failure handlers, and starting the thread are reusable wiring.

Hardware link. Background threads are the software side of the multi-core processors you study in hardware (A1.1, and HL pipelining in A1.1.6). Multiple cores are what let a background thread run at the same time as the UI thread, so the window keeps responding while the work happens. Threading itself is an IA-depth technique, not examined.

Make it your own:

  • Put your real slow work in call(). A SQLite insert from Step up to a database fits here directly; a MySQL save is the same JDBC shape.
  • Use setOnSucceeded for what happens when it works, and setOnFailed for when call() throws. Both run on the UI thread.
  • For a progress bar that fills as the work runs, a Task also has updateProgress(done, total) you can bind to a ProgressBar.

Watch out for:

  • The fx:id lblStatus must match the @FXML field name, and handleSave must exist in the controller, or the screen will not load.
  • Even inside call(), never touch a control. Do all UI updates in setOnSucceeded / setOnFailed, which JavaFX runs on the UI thread for you.
  • Task<Void> uses a type parameter and the handlers use lambdas; both are outside the exam-style Java subset but are the standard JavaFX way. Void means the task returns no value; use Task<String> (and return a value) if you need the result in setOnSucceeded via saveTask.getValue().

Mix with: Show progress toward a goal, Step up to a database when a list outgrows a file, Tell the user something.


© EduCS.me — A resource hub for Computer Science education

This site uses Just the Docs, a documentation theme for Jekyll.