Working with Lists
JavaFX provides powerful controls for displaying and manipulating lists of data. The ListView is one of the most commonly used controls for showing collections of items. In the Jukebox project, ListView is used extensively in the Admin Panel to display and manage albums and songs. This cookbook section explains how to work with ListView and ObservableList in JavaFX.
- ListView configuration and setup
- ObservableList and its importance
- Displaying data from model collections
- Selection handling
- Working with FXCollections
- Cell factories and value factories
ListView Basics
ListView is a JavaFX control that displays a list of items. Each item is rendered in its own "cell" in the list. The Jukebox project uses ListView in several places, most notably in the Admin Panel.
Example FXML from admin-panel-view.fxml:
<ListView fx:id="albumsList" prefHeight="200" prefWidth="300" HBox.hgrow="ALWAYS" />
<ListView fx:id="songsList" prefHeight="200" prefWidth="300" HBox.hgrow="ALWAYS" />
fx:id- Identifies the ListView for injection into the controllerprefHeight/prefWidth- Preferred dimensionsHBox.hgrow- Layout constraint for horizontal growth
ObservableList
ListView doesn't work with regular ArrayList or other collection types. Instead, it requires an ObservableList. This is a special type of list that can notify listeners when its contents change, allowing the ListView to automatically update its display.
Creating ObservableList from Album Model:
// In AdminPanelController.java
public void initialize() {
// Create ObservableList from the database albums
albums = FXCollections.observableArrayList(Jukebox.database.albums);
// Set it as the items for the ListView
albumsList.setItems(albums);
}
// Similarly for songs
songs = FXCollections.observableArrayList();
songsList.setItems(songs);
- Automatic UI Updates: When you add, remove, or modify items in the ObservableList, the ListView automatically updates
- Consistency: The UI always stays in sync with the data
- Performance: Only the changed items are redrawn, not the entire list
- Listener Support: You can add your own listeners to respond to changes
The FXCollections class provides factory methods for creating observable collections:
observableArrayList()- Creates an ObservableList from an existing collectionobservableArrayList(E... elements)- Creates an ObservableList with initial elementsobservableList(List<E> list)- Wraps an existing list to make it observable
Setting Up ListView
In AdminPanelController, there are dedicated methods for setting up the ListViews:
// Setup for albums ListView
private void setupAlbumsList() {
albumsList.setItems(albums);
albumsList.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
}
// Setup for songs ListView
private void setupSongsList() {
songs = FXCollections.observableArrayList();
songsList.setItems(songs);
songsList.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
}
SelectionMode.SINGLE- User can select only one item at a time (default)SelectionMode.MULTIPLE- User can select multiple items
Displaying Objects in ListView
When you add objects to a ListView, JavaFX calls the object's toString() method to get the text to display. This is why it's important for your model classes to have good toString() implementations.
In AdminPanelController:
// When an album is selected, populate the songs list
albumsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
if (newSelection != null) {
// Populate songs list with the selected album's songs
songs.setAll(newSelection.songs);
// ListView will display each Song using Song.toString()
}
}
);
In Album.java:
@Override
public String toString() {
return name + " by " + artist;
}
In Song.java:
@Override
public String toString() {
return title;
}
Result: Albums appear as "Album Name by Artist" and Songs appear as just their title.
Selection and Listeners
One of the most powerful features of ListView is the ability to respond to user selections. In AdminPanelController, selection listeners are set up for both the albums and songs ListViews:
private void setupSelectionListeners() {
// Album selection listener
albumsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
selectedAlbum = newSelection;
if (newSelection != null) {
// Populate album fields
albumNameField.setText(newSelection.name);
albumArtistField.setText(newSelection.artist);
genreComboBox.getSelectionModel().select(newSelection.genre);
// Populate songs list
songs.setAll(newSelection.songs);
} else {
// Clear fields if no album selected
albumNameField.clear();
albumArtistField.clear();
genreComboBox.getSelectionModel().select(Genre.ROCK);
songs.clear();
}
}
);
// Song selection listener
songsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
selectedSong = newSelection;
if (newSelection != null) {
// Populate song fields
songTitleField.setText(newSelection.title);
songLengthField.setText(String.valueOf(newSelection.lengthSeconds));
} else {
// Clear fields if no song selected
songTitleField.clear();
songLengthField.clear();
}
}
);
}
- Get the selection model:
listView.getSelectionModel() - Get the selected item property:
.selectedItemProperty() - Add a change listener:
.addListener((obs, oldVal, newVal) -> {...}) - In the listener, check if newVal is not null before using it
You can also get the currently selected item directly without a listener:
Album selected = albumsList.getSelectionModel().getSelectedItem();
Auto-Update with Property Listeners
In addition to selection listeners, AdminPanelController also uses property listeners on TextField values to automatically update the model when fields change:
// Album field change listeners - auto-update on change
albumNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.name = newVal;
albumsList.refresh();
}
});
albumArtistField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.artist = newVal;
albumsList.refresh();
}
});
// Song field change listeners
songTitleField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null) {
selectedSong.title = newVal;
songsList.refresh();
}
});
songLengthField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedSong != null && !newVal.isEmpty()) {
try {
selectedSong.lengthSeconds = Integer.parseInt(newVal);
songsList.refresh();
} catch (NumberFormatException e) {
// Ignore invalid input
}
}
});
- User selects an album → selection listener populates the text fields
- User types in a text field → property listener updates the album object
- Album object is updated →
refresh()is called to update ListView display - Because we're using the same album object, changes are reflected everywhere
Calling listView.refresh() forces the ListView to redraw all its cells. This is needed when:
- You modify an object that's already in the list
- You want to update the display without changing the list contents
Note: If you add or remove items from an ObservableList, you don't need to call refresh() - the ListView will update automatically.
Programmatically Selecting Items
You can programmatically select items in a ListView, which is useful when you add a new item and want it to be selected automatically:
// In AdminPanelController.onAddAlbumClicked()
public void onAddAlbumClicked(ActionEvent actionEvent) {
Album newAlbum = new Album();
newAlbum.name = "New Album";
newAlbum.artist = "Unknown Artist";
newAlbum.genre = Genre.UNKNOWN;
Jukebox.database.albums.add(newAlbum);
albums.add(newAlbum);
// Select the new album
albumsList.getSelectionModel().select(newAlbum);
}
// In AdminPanelController.onAddSongClicked()
public void onAddSongClicked(ActionEvent actionEvent) {
if (selectedAlbum == null) {
showAlert("Error", "Please select an album first");
return;
}
Song newSong = new Song();
newSong.title = "New Song";
newSong.lengthSeconds = 0;
selectedAlbum.songs.add(newSong);
songs.setAll(selectedAlbum.songs);
// Select the new song
songsList.getSelectionModel().select(newSong);
}
- Use
listView.getSelectionModel().select(item)to select a specific item - Use
select(index)to select by index - Use
selectFirst()orselectLast()to select first/last item - Use
clearSelection()to deselect all items
Customizing ListView Appearance
While ListView uses toString() by default, you can customize how items are displayed by using a cell factory. However, in the Jukebox project, the default toString() behavior is sufficient.
Example of Custom Cell Factory (not used in Jukebox):
albumsList.setCellFactory(lv -> new ListCell<Album>() {
@Override
protected void updateItem(Album album, boolean empty) {
super.updateItem(album, empty);
if (empty || album == null) {
setText(null);
} else {
// Custom display format
setText(String.format("%s - %s (%s)",
album.name, album.artist, album.genre));
}
}
});
- You want to display more information than just toString()
- You want custom formatting or colors
- You want to include graphics or icons
- You want interactive cells with buttons
Clearing and Updating ListView
Sometimes you need to clear or update the entire ListView. In AdminPanelController, this happens when deleting albums or songs:
// In AdminPanelController.onDeleteAlbumClicked()
public void onDeleteAlbumClicked(ActionEvent actionEvent) {
Album selected = albumsList.getSelectionModel().getSelectedItem();
if (selected == null) {
showAlert("Error", "Please select an album to delete");
return;
}
if (showConfirmation("Delete Album", "Are you sure?")) {
albums.remove(selected);
Jukebox.database.albums.remove(selected);
// Clear selection
selectedAlbum = null;
albumNameField.clear();
albumArtistField.clear();
genreComboBox.getSelectionModel().select(Genre.ROCK);
songs.clear(); // Clear the songs list
}
}
// In AdminPanelController.onDeleteSongClicked()
public void onDeleteSongClicked(ActionEvent actionEvent) {
Song selected = songsList.getSelectionModel().getSelectedItem();
if (selected == null || selectedAlbum == null) {
showAlert("Error", "Please select a song to delete");
return;
}
if (showConfirmation("Delete Song", "Are you sure?")) {
selectedAlbum.songs.remove(selected);
songs.setAll(selectedAlbum.songs); // Update the ObservableList
// Clear selection
selectedSong = null;
songTitleField.clear();
songLengthField.clear();
}
}
ObservableList.add(item)- Add a new item (ListView updates automatically)ObservableList.remove(item)- Remove an item (ListView updates automatically)ObservableList.clear()- Remove all items (ListView updates automatically)ObservableList.setAll(collection)- Replace all items with a new collection (ListView updates automatically)
Try This:
- Run the Jukebox application and go to Admin Panel
- Add a few albums and see how they appear in the albums ListView
- Select an album and see how the album details appear in the fields
- Select an album and add songs to it - see how they appear in the songs ListView
- Edit an album name in the text field and see how the ListView updates automatically
- Look at
AdminPanelController.javaand find the setupAlbumsList() and setupSongsList() methods - Find the selection listeners and trace through how they work
Common Pitfalls
If you modify the list that backs a ListView while the ListView is in the middle of an update, you might get unexpected behavior. Always use ObservableList and let it handle the updates.
When you modify an object that's already in an ObservableList, the ListView won't automatically update because the list itself hasn't changed. You need to call refresh() to force a redraw.
If your model objects have a toString() that returns null, ListView will throw a NullPointerException. Always ensure toString() returns a valid string.
Always check if the selected item is null in your selection listeners. Users can click on empty space or deselect all items.
ListView works well for small to medium lists, but for very large lists (thousands of items), performance can degrade. For large datasets, consider using Virtualized controls or pagination.
Key Takeaways
- ListView displays a list of items from an ObservableList
- ObservableList automatically notifies ListView when contents change
- Use FXCollections.observableArrayList() to create ObservableLists
- ListView uses toString() to display items by default
- Selection listeners respond to user selection changes
- Property listeners on TextFields can auto-update model objects
- Call refresh() when modifying objects in place
- Use selection model to programmatically select items
- Set selection mode to SINGLE or MULTIPLE as needed