Фоновые задачи: 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.