A cup of steaming hot java with the letters

My JavaFX ListViews are doubling up

18 January 2022

You’re making a JavaFX app and you want to render a collection properly by using a ListView or TableView. These components are great: you point them to an ObservableList and all you need to do is update this list and the views will automatically be updated. You can also generate a bunch of your favourite JavaFX UI elements for each item in the list — including buttons, labels, inputs — any components at all!

There’s a problem you might run into though. We’ll see it soon.

Hello ListView #

We’ll start with the “hello world” of JavaFX list views. The code below is essentially what you’d find in the JavaFX ListView docs. But I’ve rendered inside of an application scene:


    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.ListView;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;

    public class Main extends Application {

            @Override
            public void start(Stage primaryStage) throws Exception {

                     ObservableList<String> names = FXCollections.observableArrayList(
                              "Julia", "Ian", "Sue", "Matthew", "Hannah", "Stephan", "Denise");
                     ListView<String> listView = new ListView<String>(names);

                    VBox pane = new VBox();
                    pane.getChildren().add(listView);
                    Scene scene = new Scene(pane, 700, 500);
                    primaryStage.setScene(scene);

                    primaryStage.show();
            }
    public static void main(String[] args) {
            launch(args);
    }
}

This works fine and prints off a nice list of names.

Cell factories #

What if we wanted to customize our ListView?

Let’s make it render the following for each item in our observable list:

We’ll wrap these in an HBox component so they’re nicely displayed horizontally on the screen.

You can do this using a cell factory - which needs to be a class that implements the JavaFX CallBack interface. In this class we override the call method to return a new ListCell object.

In the ListCell object we return, we want to override the updateItem method and call setGraphic inside. In the arguments of setGraphic, we pass the component that should be created for each cell. We’re going to pass it our HBox after we’ve created it and filled it with the list of components from before.

I promise it’s not too bad. Here’s what it looks like:


    /* Main.java
    Do not write your JavaFX like this - we need to fix this up!
    */
    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.Region;
    import javafx.scene.layout.VBox;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    import javafx.util.Callback;

    public class Main extends Application {

            @Override
            public void start(Stage primaryStage) throws Exception {

                     ObservableList<String> names = FXCollections.observableArrayList(
                              "Julia", "Ian", "Sue", "Matthew", "Hannah", "Stephan", "Denise");
                     ListView<String> listView = new ListView<String>(names);

                     class PersonCellFactory implements Callback<ListView<String>, ListCell<String>> {

                                    @Override
                                    public ListCell<String> call(ListView<String> listView) {
                                               return new ListCell<String>(){
                                                @Override
                                                public void updateItem(String nameString, boolean empty) {
                                                    super.updateItem(nameString, empty);
                                                    Text nameText = new Text(nameString);
                                                    Region region1 = new Region();
                                                    TextField nameField = new TextField(nameString);
                                                    Button changeNameButton = new Button("update");
                                                    HBox box = new HBox();
                                                    HBox.setHgrow(region1, Priority.ALWAYS);
                                                    box.setAlignment(Pos.BASELINE_LEFT);
                                                    box.getChildren().addAll(nameText,region1,nameField,changeNameButton);
                                                    setGraphic(box);
                                                }
                                            };
                                    }
                            }
                    listView.setCellFactory(new PersonCellFactory());
                    VBox pane = new VBox();
                    pane.getChildren().add(listView);
                    Scene scene = new Scene(pane, 700, 500);
                    primaryStage.setScene(scene);

                    primaryStage.show();
            }
    public static void main(String[] args) {
            launch(args);
    }
}

If you run this you’ll get something a little weird, where you get a clickable list of names, then a big list of items underneath in which the button and texfield is clickable, but not the rest. This is sort of what we want, but not really.

Checking for empty cells #

What we need to do is add a check in updateItem to make sure that if it’s an empty cell, we don’t add anything in. This is what you might think this should like:


    @Override
    public void updateItem(String nameString, boolean empty) {

            super.updateItem(nameString, empty);
            /*
            * Uh oh! This is buggy! Don't worry we'll fix it soon
            */
            if (empty) {
                    return;
            }
            Text nameText = new Text(nameString);
            Region region1 = new Region();
            TextField nameField = new TextField(nameString);
            Button changeNameButton = new Button("update");
            HBox box = new HBox();
            HBox.setHgrow(region1, Priority.ALWAYS);
            box.setAlignment(Pos.BASELINE_LEFT);
            box.getChildren().addAll(nameText, region1, nameField, changeNameButton);
            setGraphic(box);
    }

Okay, when we run it it seems okay? We’re only rendering the items that aren’t empty - just what we wanted.

But let’s say that we add a button afterwards to add a new person to our list:


    listView.setCellFactory(new PersonCellFactory());
    /* New code from here */
    Button newPersonButton = new Button("Add person");
    newPersonButton.setOnAction(event->{
            names.add("Johnny");
    });
    VBox pane = new VBox();
    pane.getChildren().add(listView,newPersonButton);
    /* New code ends here */
    Scene scene = new Scene(pane, 700, 500);
    /* etc */

When we add this, everything looks fine to start with, but when we click the newPersonButton we run into our problem. The new item, “Johnny”, gets added, but all the other items seem to appear under it in reverse order! What is going on?

Why isn’t my JavaFX ListView working? #

This is something to do with the internals of the way the ListView interacts with cell factories. What we need to do when the cell is empty is not just return, but set the graphic as null.

This is because the updateItem method is used not only to create the cell, but also to clean it up!

This is how our check should look:

if (empty) {
/* This is the important part */
setGraphic(null);
return;
}

And here’s how the whole program should look at this point:

    /* Main.java
    Do not write your JavaFX like this - we need to fix this up!
    */

    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.Region;
    import javafx.scene.layout.VBox;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    import javafx.util.Callback;

    public class Main extends Application {

            @Override
            public void start(Stage primaryStage) throws Exception {

                    ObservableList<String> names = FXCollections.observableArrayList("Julia", "Ian", "Sue", "Matthew", "Hannah",
                                    "Stephan", "Denise");
                    ListView<String> listView = new ListView<String>(names);

                    class PersonCellFactory implements Callback<ListView<String>, ListCell<String>> {

                            @Override
                            public ListCell<String> call(ListView<String> listView) {
                                    return new ListCell<String>() {
                                            @Override
                                            public void updateItem(String nameString, boolean empty) {

                                                    super.updateItem(nameString, empty);
                                                    if (empty ) {
                                                            /* This is the important part */
                                                            setGraphic(null);
                                                            return;
                                                    }
                                                    Text nameText = new Text(nameString);
                                                    Region region1 = new Region();
                                                    TextField nameField = new TextField(nameString);
                                                    Button changeNameButton = new Button("update");
                                                    HBox box = new HBox();
                                                    HBox.setHgrow(region1, Priority.ALWAYS);
                                                    box.setAlignment(Pos.BASELINE_LEFT);
                                                    box.getChildren().addAll(nameText, region1, nameField, changeNameButton);
                                                    setGraphic(box);
                                            }
                                    };
                            }
                    }

                    listView.setCellFactory(new PersonCellFactory());

                    Button newPersonButton = new Button("Add person");
                    newPersonButton.setOnAction(event -> {
                            names.add("Johnny");
                    });
                    VBox pane = new VBox();
                    pane.getChildren().addAll(listView, newPersonButton);
                    Scene scene = new Scene(pane, 700, 500);
                    primaryStage.setScene(scene);

                    primaryStage.show();
            }

            public static void main(String[] args) {
                    launch(args);
            }
    }

But wait! This still isn't ideal. There's a better way!!!!

We're doing too much work in our updateItem method! A big thank you to Dirk Lemmermann for pointing this out.

Reducing the amount of work in the updateItem method #

It turns out the updateItem method gets called all the time. This includes getting called in response to select and focus events as well as changes in the data model. At the moment our code is rebuilding the box and all of its child components from scratch for every item, every single time one of these events happens.

To reduce the amount of work that we're doing in our updateItem method, we can build the child components once in the cell factory (or if you make a custom cell class: in the constructor). After that, the only thing our updateItem method needs to do, is call setText on the nameText and nameField components.

To stop the cell from showing up when it's empty we can use a superpower JavaFX provides: Binding. We want to bind the visibility property of the HBox to the inverse of the empty property of the ListCell. Here's what this looks like:

box.visibleProperty().bind(cell.emptyProperty().not());

And here's how it looks in context:


import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Main extends Application {

	@Override
	public void start(Stage primaryStage) throws Exception {

		ObservableList<String> names = FXCollections.observableArrayList("Julia", "Ian", "Sue", "Matthew", "Hannah",
				"Stephan", "Denise");
		ListView<String> listView = new ListView<String>(names);

		class PersonCellFactory implements Callback<ListView<String>, ListCell<String>> {

			@Override
			public ListCell<String> call(ListView<String> listView) {

				Text nameText = new Text();
				Region region1 = new Region();
				TextField nameField = new TextField();
				Button changeNameButton = new Button("update");
				HBox.setHgrow(region1, Priority.ALWAYS);
				HBox box = new HBox();
				box.setAlignment(Pos.BASELINE_LEFT);
				box.getChildren().addAll(nameText, region1, nameField, changeNameButton);

				ListCell<String> cell = new ListCell<String>() {
					public void updateItem(String nameString, boolean empty) {
						super.updateItem(nameString, empty);
						nameText.setText(nameString);
						nameField.setText(nameString);
					}
				};
				cell.setGraphic(box);
				box.visibleProperty().bind(cell.emptyProperty().not());
				return cell;
			}
		}

		listView.setCellFactory(new PersonCellFactory());

		Button newPersonButton = new Button("Add person");
		newPersonButton.setOnAction(event -> {
			names.add("Johnny");
		});
		VBox pane = new VBox();
		pane.getChildren().addAll(listView, newPersonButton);
		Scene scene = new Scene(pane, 700, 500);
		primaryStage.setScene(scene);

		primaryStage.show();
	}

	public static void main(String[] args) {
		launch(args);
	}
}

Nice! Custom JavaFX ListViews behaving just the way we want them to and not doing to much work to recreate the UI. 😌

Now onto writing the code to edit the items. . .

Back to blog