ListView and ObservableList
Time to store notes for real. Each note becomes a Note record object, and all of them become elements of an observable list, ObservableList. It is an ordinary List that tells its subscribers about every addition and removal. A ListView subscribes to such a list and redraws itself: add an element to the list and it appears on screen.
The Note record
Create a file for the model:
// src/main/java/com/example/notes/Note.java
package com.example.notes;
public record Note(String title, String text, String category, boolean important) {
}A record is immutable: to change a note, you create a new one and replace the old one in the list with it.
The list and the ListView
// src/main/java/com/example/notes/App.java — class fields
private final ObservableList<Note> notes = FXCollections.observableArrayList();
private final ListView<Note> listView = new ListView<>(notes);FXCollections.observableArrayList() creates an empty observable list. The ListView gets it in the constructor and shows its elements. To start with, let's put two notes in the list:
// src/main/java/com/example/notes/App.java — part of start()
notes.addAll(
new Note("Buy milk", "2 liters", "Home", true),
new Note("Call Oleg", "Discuss the vacation", "Work", false));How an element is shown
By default a ListView prints the element's toString(), which for a record is Note[title=Buy milk, text=2 liters, ...]. To show only the title, set a cell factory, an object that creates the rows of the list:
// src/main/java/com/example/notes/App.java — part of start()
listView.setCellFactory(view -> new ListCell<>() {
@Override
protected void updateItem(Note note, boolean empty) {
super.updateItem(note, empty);
if (empty || note == null) {
setText(null);
} else {
setText(note.important() ? "★ " + note.title() : note.title());
}
}
});A ListView creates only a few cells for the visible part and reuses them while scrolling. The updateItem method is called when a cell gets another element or becomes empty, so the empty case must always be handled.
Selecting, adding and deleting
The selected element lives in the selection model, getSelectionModel(). Let's subscribe to it to show the note in the form:
// src/main/java/com/example/notes/App.java — part of the class
listView.getSelectionModel().selectedItemProperty().addListener(
(observable, oldNote, note) -> {
if (note != null) {
showNote(note);
}
});
// ...
private void addNote(ActionEvent event) {
Note note = new Note(
titleField.getText().strip(),
textArea.getText(),
categoryBox.getValue(),
importantBox.isSelected());
notes.add(note);
listView.getSelectionModel().select(note);
}
private void deleteNote(ActionEvent event) {
int index = listView.getSelectionModel().getSelectedIndex();
if (index >= 0) {
notes.remove(index);
}
}We change only the notes list, and the ListView updates itself. The showNote method fills the form fields with the note's values.
Bindings to the list
The counter and the "Delete" button are bound just like in the previous lesson:
// src/main/java/com/example/notes/App.java — part of start()
deleteButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull());
Label countLabel = new Label();
countLabel.textProperty().bind(Bindings.size(notes).asString("Notes: %d"));Bindings.size(notes) is the number of list elements as a binding. Our own count property is no longer needed.
The whole class
// src/main/java/com/example/notes/App.java
package com.example.notes;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
private final ObservableList<Note> notes = FXCollections.observableArrayList();
private final ListView<Note> listView = new ListView<>(notes);
private final TextField titleField = new TextField();
private final TextArea textArea = new TextArea();
private final ComboBox<String> categoryBox = new ComboBox<>();
private final CheckBox importantBox = new CheckBox("Important");
@Override
public void start(Stage stage) {
notes.addAll(
new Note("Buy milk", "2 liters", "Home", true),
new Note("Call Oleg", "Discuss the vacation", "Work", false));
listView.setCellFactory(view -> new ListCell<>() {
@Override
protected void updateItem(Note note, boolean empty) {
super.updateItem(note, empty);
if (empty || note == null) {
setText(null);
} else {
setText(note.important() ? "★ " + note.title() : note.title());
}
}
});
listView.getSelectionModel().selectedItemProperty().addListener(
(observable, oldNote, note) -> {
if (note != null) {
showNote(note);
}
});
textArea.setPrefRowCount(5);
textArea.setWrapText(true);
categoryBox.getItems().addAll("Work", "Home", "Ideas");
categoryBox.setValue("Ideas");
Button addButton = new Button("Add");
addButton.setOnAction(this::addNote);
addButton.disableProperty().bind(titleField.textProperty().isEmpty());
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(this::deleteNote);
deleteButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull());
Label countLabel = new Label();
countLabel.textProperty().bind(Bindings.size(notes).asString("Notes: %d"));
GridPane form = new GridPane();
form.setHgap(8);
form.setVgap(8);
form.setPadding(new Insets(0, 0, 0, 16));
form.add(new Label("Title:"), 0, 0);
form.add(titleField, 1, 0);
form.add(new Label("Text:"), 0, 1);
form.add(textArea, 1, 1);
form.add(new Label("Category:"), 0, 2);
form.add(categoryBox, 1, 2);
form.add(importantBox, 1, 3);
ColumnConstraints labels = new ColumnConstraints();
labels.setMinWidth(Region.USE_PREF_SIZE);
ColumnConstraints fields = new ColumnConstraints();
fields.setHgrow(Priority.ALWAYS);
form.getColumnConstraints().addAll(labels, fields);
VBox listPane = new VBox(8, listView, countLabel);
listPane.setPrefWidth(200);
VBox.setVgrow(listView, Priority.ALWAYS);
HBox buttons = new HBox(8, addButton, deleteButton);
buttons.setPadding(new Insets(12, 0, 0, 0));
Label header = new Label("My notes");
BorderPane root = new BorderPane(form, header, null, buttons, listPane);
root.setPadding(new Insets(16));
BorderPane.setMargin(header, new Insets(0, 0, 12, 0));
stage.setTitle("Notes");
stage.setScene(new Scene(root, 640, 480));
stage.show();
}
private void addNote(ActionEvent event) {
Note note = new Note(
titleField.getText().strip(),
textArea.getText(),
categoryBox.getValue(),
importantBox.isSelected());
notes.add(note);
listView.getSelectionModel().select(note);
}
private void deleteNote(ActionEvent event) {
int index = listView.getSelectionModel().getSelectedIndex();
if (index >= 0) {
notes.remove(index);
}
}
private void showNote(Note note) {
titleField.setText(note.title());
textArea.setText(note.text());
categoryBox.setValue(note.category());
importantBox.setSelected(note.important());
}
public static void main(String[] args) {
launch(args);
}
}On the left is a list of "★ Buy milk" and "Call Oleg", with "Notes: 2" under it. Click a note and its data appears in the form, and the "Delete" button becomes enabled. Change the title to "Read a book" and click "Add": the list now has three rows, and the new one is selected.
Common mistake
The data is kept in an ordinary ArrayList, and the ListView gets FXCollections.observableArrayList(list). That method copies the elements into a new list, so later list.add(...) calls never show up on screen, and there is no error either. Keep the notes in a single ObservableList and change that list.