Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Фоновые задачи: Task и Platform.runLater

Все обработчики JavaFX выполняются в одном потоке — JavaFX Application Thread. Этот же поток перерисовывает окно. Если в нём читать большой файл или ждать ответа сервера, окно на это время «замёрзнет»: кнопки не нажимаются, а система может пометить программу как «Не отвечает». Поэтому долгую работу уносят в фоновый поток, а в поток JavaFX возвращаются только с готовым результатом.

Главное правило

ГдеЧто можно
Фоновый потокЧитать файлы, обращаться к сети и базе, считать
Поток JavaFXМенять узлы, списки на экране, открывать окна

Из фонового потока интерфейс трогать нельзя. Чтобы выполнить код в потоке JavaFX, его передают в Platform.runLater.

Поток и Platform.runLater

Загрузим заметки в виртуальном потоке и передадим результат в интерфейс:

// src/main/java/com/example/notes/MainController.java — первый вариант loadNotes()
private void loadNotes() {
    statusLabel.setText("Загрузка...");
    Thread.ofVirtual().start(() -> {
        List<Note> loaded = storage.load();
        Platform.runLater(() -> {
            notes.setAll(loaded);
            notes.addListener((ListChangeListener<Note>) change -> storage.save(notes));
            statusLabel.setText("Загружено заметок: " + loaded.size());
        });
    });
}

storage.load() работает в фоне, а лямбда внутри runLater встаёт в очередь потока JavaFX и выполняется, когда он освободится. Слушатель сохранения подключается только после загрузки — иначе пустой список мог бы перезаписать файл.

Строка состояния

Для сообщений добавим в нижний ряд индикатор и надпись:

<!-- src/main/resources/com/example/notes/main-view.fxml — нижний ряд -->
<HBox spacing="8" alignment="CENTER_LEFT">
    <padding>
        <Insets top="12"/>
    </padding>
    <Button fx:id="addButton" text="Добавить" onAction="#addNote"/>
    <Button fx:id="deleteButton" text="Удалить" onAction="#deleteNote"
            styleClass="danger"/>
    <ProgressIndicator fx:id="progress" prefWidth="20" prefHeight="20" visible="false"/>
    <Label fx:id="statusLabel"/>
</HBox>

Не забудьте <?import javafx.scene.control.ProgressIndicator?> в начале файла и поля в контроллере:

// src/main/java/com/example/notes/MainController.java — новые поля
@FXML
private ProgressIndicator progress;
@FXML
private Label statusLabel;

Task

Task — класс JavaFX для фоновой работы. Его метод call() выполняется в фоне, а обработчики onSucceeded и onFailed — уже в потоке JavaFX, поэтому runLater не нужен. Ещё у задачи есть свойства running, progress и message, к которым удобно привязать интерфейс. В initialize() вместо прежних двух строк загрузки остаётся вызов loadNotes():

// src/main/java/com/example/notes/MainController.java — метод loadNotes() на 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.size());
    });
    task.setOnFailed(event -> {
        statusLabel.setText("Не удалось прочитать файл заметок");
        task.getException().printStackTrace();
    });
    progress.visibleProperty().bind(task.runningProperty());
    statusLabel.setText("Загрузка...");
 
    Thread thread = new Thread(task);
    thread.setDaemon(true);
    thread.start();
}

task.getValue() — то, что вернул call(). Индикатор виден, пока задача выполняется. Поток помечен как служебный (setDaemon(true)), чтобы незаконченная задача не мешала программе завершиться после закрытия окна.

Проверка

Запустите приложение. На долю секунды внизу мелькнёт «Загрузка...» с крутящимся индикатором, затем появится «Загружено заметок: 2». Теперь испортьте notes.json, поставив лишнюю запятую, и запустите снова: окно откроется с пустой таблицей и надписью «Не удалось прочитать файл заметок», а текст ошибки Jackson будет в терминале. Слушатель сохранения не подключён, поэтому испорченный файл не перезапишется пустым списком.

Частая ошибка

Интерфейс меняют прямо из фонового потока. Иногда это «работает» без ошибок, но приводит к случайным сбоям отрисовки, а часть операций JavaFX проверяет поток и сразу падает. Например, new Alert(...).show() внутри new Thread(...) даёт java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4. Всё, что касается узлов, списков на экране и окон, выполняйте в onSucceeded, onFailed или внутри Platform.runLater.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.