History of the API
JavaFX, in its current form, was first released as version 2.0 (version 1.x was entirely different from the perspective of the developer, and used a scripting language). Version 2.0 was released in 2011. That placed it before the release of Java 8, which meant that many modern language features (such as lambda expressions and functional interfaces) were not available.
JavaFX makes extensive use of bindings and properties. These properties are objects wrapping a single variable and providing get
and set
methods for accessing and modifying the value. In addition to the accessor and modifier, they also provide a mechanism to register a listener which will be notified when the value changes. This makes writing applications using a MVC (or related, such as MVP or MVVM) architecture much more straightforward, as the model can be implemented with these properties making notification to the view very easy.
One requirement that arises from time to time is the need to observe or bind to a "property of a property" (some examples are shown below). Prior to lambda expressions, an API that supported this in a robust, typesafe manner would have been very unwieldy to use. The compromise API that JavaFX 2.0 used was the Bindings.select
API. While this API works, it relies on reflection, has no compile-time checking, and does not handle null results elegantly.
After the release of Java 8 and JavaFX 8, a feature request was filed to replace Bindings.select(...)
with a more robust solution. An implementation was written by Tomas Mikula and included in his third party libraries: initially in the (no longer maintained) Easy Bind and also in ReactFX (which also seems to be no longer maintained). As of JavaFX 19, some of this functionality was rolled into the library. Great credit should go to Tomas Mikula for pioneering this.
Simple Example: map()
Suppose we have a class with an immutable value. Here we'll implement this as a record
:
record Person(String name){}
We can have a ListView<Person>
which uses a custom list cell to display the person's name:
ListView<Person> list = new ListView<>();
list.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty || person == null) {
setText("");
} else {
setText(person.name());
}
}
});
Suppose (somewhat artificially) we want a label to display the name of the currently-selected person. A naïve attempt to do this might look like
Label selectedPerson = new Label();
selectedPerson.textProperty().bind(
list.getSeletionModel().selectedItemProperty().asString()
);
The problem with this is that asString()
will call String.valueOf()
on the selected Person
. This will give the literal string "null"
if the selected person is null
(i.e. there is no selection) and will give the result of Person.toString()
otherwise. This will not give the desired display.
Instead, we can use map
:
selectedItemProperty().map(Person::name)
results in an ObservableValue<String>
. This observable value holds the value null
if selectedItemProperty()
is null, and holds the result of selectedItemProperty().getValue().name()
otherwise. If selectedItemProperty()
changes, the ObservableValue<String>
is automatically updated, and notifies its listeners (including ones registered by any bindings to it).
So we can do
ObservableValue<String> name = list.getSelectionModel().selectedItemProperty().map(Person::name);
selectedPerson.textProperty().bind(name);
If nothing is selected, then name
contains null, and the text is set to null
, which is allowed by a Label
. If a person is selected, the text is set to the person's name, exactly as required.
Here is the complete example:
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
record Person(String name) {}
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
Label selectedPerson = new Label();
ListView<Person> list = new ListView<>();
list.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty || person == null) {
setText("");
} else {
setText(person.name());
}
}
});
ObservableValue<String> selectedName = list.getSelectionModel().selectedItemProperty()
.map(Person::name);
selectedPerson.textProperty().bind(selectedName);
BorderPane root = new BorderPane();
root.setTop(new HBox(5, new Label("Selection:"), selectedPerson));
root.setCenter(list);
List.of("James", "Jennifer", "Jim", "Joanne", "John").stream()
.map(Person::new)
.forEach(list.getItems()::add);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
In the first example of using a "property of a property" the initial property was the selected person, and the property of the property was the person's name, which was immutable. Hence the only change we needed to care about was a change to the selected person.
In more complicated scenarios, the "property of the property" might itself be some kind of observable value.
Here is a very basic clock implementation:
import javafx.animation.AnimationTimer;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Clock {
private final Label label ;
private final AnimationTimer timer ;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h:mm:ss a");
private final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>(LocalTime.now());
private final ObservableValue<String> formattedTime = time.map(formatter::format);
public Clock() {
this.label = new Label();
label.textProperty().bind(formattedTime);
timer = new AnimationTimer() {
@Override
public void handle(long now) {
time.set(LocalTime.now());
}
};
timer.start();
}
public void start() {
System.out.println("Starting");
timer.start();
}
public void stop() {
System.out.println("Stopping");
timer.stop();
}
public Node getView() {
return label;
}
public ReadOnlyObjectProperty<LocalTime> timeProperty() {
return time.getReadOnlyProperty();
}
public ObservableValue<String> formattedTimeProperty() {
return formattedTime;
}
public String getFormattedTime() {
return formattedTime.getValue();
}
}
It contains a label, which displays the time (as text). An AnimationTimer
updates the label every time the JavaFX system renders a frame, setting its text to the current time.
Notice that the timer is always running. While this probably doesn't matter much in practice, it would be good to run the timer only when the label is displayed in a window. How do we find that out?
The label
has a sceneProperty()
which references the current scene in which the label
is displayed (or null
if it is not in a scene). In turn, the Scene
has a windowProperty()
which references the window containing the scene (or null, if the scene is not in a window). Finally, the window has a showingProperty()
, which is a BooleanProperty
representing whether or not the window is showing.
So to get the current window containing the label, we need logic of the form
private boolean isLabelShowing(Label label) {
Scene scene = label.sceneProperty().get();
if (scene == null) {
return false ;
}
Window window = scene.windowProperty().get();
if (window == null) {
return false;
}
return window.showingProperty().get();
}
This needs to be recomputed if
- The label is placed in a new scene (or removed from a scene)
- The scene containing the label is placed in a new window (or removed from a window)
- The window is shown or hidden
Note that using map()
here doesn't give us what we want.
label.sceneProperty().map(Scene::windowProperty)
would return an ObservableValue<ReadOnlyObjectProperty<Window>>
. This is not the correct type, and would not change if the scene was placed in a different window (the value contained by windowProperty()
would change, but the ReadOnlyObjectProperty<Window>
itself would still be the same object.
The flatMap()
method addresses this problem:
label.sceneProperty()
.flatMap(Scene::windowProperty)
is an ObservableValue<Window>
. Its value is the window containing the scene containing the label. It will be null if label.getScene()
is null, or if label.getScene().getWindow()
is null. It will fire updates if either the label is placed in a different scene, or if the scene is placed in a different window.
Similarly,
label.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
is an ObservableValue<Boolean>
. It will contain null
if label.sceneProperty().flatMap(Scene::windowProperty)
contains null
(i.e. if label.getScene()
is null
or if label.getScene().getWindow()
is null
), and will otherwise contain the (boxed) result of calling label.getScene().getWindow().isShowing()
.
So we can make the timer for our Clock
automatically start when it is in a scene which is in a window which is showing, and automatically stop when that is no longer true:
import javafx.animation.AnimationTimer;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Clock {
private final Label label ;
private final AnimationTimer timer ;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h:mm:ss a");
private final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>(LocalTime.now());
private final ObservableValue<String> formattedTime = time.map(formatter::format);
public Clock() {
this.label = new Label();
label.textProperty().bind(formattedTime);
timer = new AnimationTimer() {
@Override
public void handle(long now) {
time.set(LocalTime.now());
}
};
ObservableValue<Boolean> showing = label.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(window -> window.showingProperty());
showing.addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing != null || isNowShowing.booleanValue()) {
start();
} else {
stop();
}
});
}
public void start() {
System.out.println("Starting");
timer.start();
}
public void stop() {
System.out.println("Stopping");
timer.stop();
}
public Node getView() {
return label;
}
public ReadOnlyObjectProperty<LocalTime> timeProperty() {
return time.getReadOnlyProperty();
}
public ObservableValue<String> formattedTimeProperty() {
return formattedTime;
}
public String getFormattedTime() {
return formattedTime.getValue();
}
}
Here is a quick test app for this:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
public class ClockApp extends Application {
@Override
public void start(Stage stage) throws IOException {
VBox root = new VBox(5);
root.setPadding(new Insets(10));
Clock clock = new Clock();
VBox clockBox = new VBox(5, new Label("Current Time:"), clock.getView());
ToggleButton showClock = new ToggleButton("Show Time");
showClock.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
root.getChildren().add(clockBox);
} else {
root.getChildren().remove(clockBox);
}
});
root.getChildren().add(showClock);
Scene scene = new Scene(root, 400, 400);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
In the previous example we had to specifically handle the possibility of a null
value:
ObservableValue<Boolean> showing = label.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(window -> window.showingProperty());
showing.addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing != null && isNowShowing.booleanValue()) {
start();
} else {
stop();
}
});
The issue here is that showing
is an ObservableValue
wrapping a Boolean
object reference (not a boolean
primitive). That reference can take three possible values: Boolean.TRUE
, Boolean.FALSE
, or null
. It will actually take on a null
value if any of the steps in the "chain" of flatMap
calls results in a reference which is null. This will occur, for example, if the label is not in a scene (so label.sceneProperty()
contains null
) or the scene is not in a window. In this case, calling isNowShowing.booleanValue()
(or implicitly unboxing with code such as if (isNowShowing){...}
) will throw a null pointer exception.
In this example, if showing
contains null
, it means the label is not in a window, and so we want to treat it equivalently to the way we treat Boolean.FALSE
. The orElse(...)
method creates an ObservableValue
which contains the same value as the original ObservableValue
if it is not null, and the provided "default" value if it is null. So we can clean up this section of code:
ObservableValue<Boolean> showing = label.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(window -> window.showingProperty())
.orElse(Boolean.FALSE);
showing.addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
start();
} else {
stop();
}
});
or (in an even more fluid style):
label.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(window -> window.showingProperty())
.orElse(Boolean.FALSE)
.addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
start();
} else {
stop();
}
});
The when()
method creates a new ObservableValue
that only fires updates when the provided ObservableValue<Boolean>
is true (or becomes true, if the underlying value has changed).
To demonstrate this, we can add an alarm to our clock, and a check box which activates the alarm. To keep things simple, our alarm will just go off every minute (i.e. when the seconds reach zero). We will fire a notification (via an Alert
box) when the alarm is triggered, but only if the check box is selected.
We can use the previous techniques to create an ObservableValue
that changes to true
when the seconds reach zero (and will change back to false
a second later):
clock.timeProperty()
.map(time -> time.getSecond() == 0)
will contain true
when the seconds portion of the time is zero, and false
otherwise. We can use when()
to create an observable that only fires notifications to its listeners if it changes when a checkbox is checked:
CheckBox alarm = new CheckBox("Activate Alarm");
Alert alert = new Alert(Alert.AlertType.INFORMATION);
clock.timeProperty()
.map(time -> time.getSecond() == 0)
.when(alarm.selectedProperty())
.addListener((obs, oldValue, isNewMinute) -> {
if (isNewMinute) {
alert.setContentText("Time is " + clock.getFormattedTime());
alert.show()
}
});
Use in Virtualized Control Cells
The map()
and flatMap()
methods can provide some nice solutions for custom cell implementations in virtualized controls (such as ListView
and TableView
).
For example, the first example in this post had a ListView
defined as
ListView<Person> list = new ListView<>();
list.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty || person == null) {
setText("");
} else {
setText(person.name());
}
}
});
The cell factory here returns a custom cell that sets the text to null
if the item is null, or to the result of calling name()
on the item otherwise. This can be accomplished essentially in a single line, and without subclassing ListCell
, by binding the text property:
list.setCellFactory(lv -> {
ListCell<Person> cell = new ListCell<>();
cell.textProperty().bind(cell.itemProperty().map(Person::name));
return cell;
});
For a more complex example, consider a stock monitoring application which displays a table of stocks and their prices, which may change at any time. We want to style the rows depending on whether the stock has recently increased or decreased in price, using the following style sheet:
.table-row-cell:recently-increased {
-fx-control-inner-background: #c0c0ff;
}
.table-row-cell:recently-decreased {
-fx-control-inner-background: #ffa0a0;
}
The Stock
class is implemented as follows. It has two read/write properties (a name and a price), and two read-only properties indicating if the price has recently increased or decreased. When the price changes, one of those properties is set to true, and a PauseTransition
is turns that off after a fixed period of time:
import javafx.animation.PauseTransition;
import javafx.beans.property.*;
import javafx.util.Duration;
public class Stock {
private static final Duration RECENT_CHANGE_EXPIRATION = Duration.seconds(2);
private final StringProperty name = new SimpleStringProperty();
private final DoubleProperty price = new SimpleDoubleProperty();
private final ReadOnlyBooleanWrapper recentlyIncreased = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper recentlyDecreased = new ReadOnlyBooleanWrapper();
private final PauseTransition resetDelay = new PauseTransition(RECENT_CHANGE_EXPIRATION);
public Stock(String name, double price) {
setName(name);
setPrice(price);
resetDelay.setOnFinished(e -> {
recentlyIncreased.set(false);
recentlyDecreased.set(false);
});
priceProperty().addListener((obs, oldPrice, newPrice) -> {
resetDelay.stop();
recentlyIncreased.set(newPrice.doubleValue() > oldPrice.doubleValue());
recentlyDecreased.set(newPrice.doubleValue() < oldPrice.doubleValue());
resetDelay.playFromStart();
});
}
public final String getName() {
return nameProperty().get();
}
public StringProperty nameProperty() {
return name;
}
public final void setName(String name) {
this.nameProperty().set(name);
}
public final double getPrice() {
return priceProperty().get();
}
public DoubleProperty priceProperty() {
return price;
}
public final void setPrice(double price) {
this.priceProperty().set(price);
}
public void adjustPrice(double percentage) {
setPrice((1+percentage/100) * getPrice());
}
public boolean isRecentlyIncreased() {
return recentlyIncreased.get();
}
public ReadOnlyBooleanWrapper recentlyIncreasedProperty() {
return recentlyIncreased;
}
public boolean isRecentlyDecreased() {
return recentlyDecreased.get();
}
public ReadOnlyBooleanWrapper recentlyDecreasedProperty() {
return recentlyDecreased;
}
}
To style the rows we set the recently-increased
or recently-decreased
CSS pseudoclasses depending on the state of the item in the row. A rowFactory
with a couple of listeners mapping the properties to the CSS pseudoclasses is all that is needed:
TableView<Stock> stockTable = new TableView<>();
// ...
stockTable.setRowFactory(tv -> {
TableRow<Stock> row = new TableRow<>();
row.itemProperty()
.flatMap(Stock::recentlyIncreasedProperty)
.orElse(false)
.addListener(
(obs, wasIncreased, isNowIncreased) -> row.pseudoClassStateChanged(INCREASED_PC, isNowIncreased)
);
row.itemProperty()
.flatMap(Stock::recentlyDecreasedProperty)
.orElse(false)
.addListener(
(obs, wasDecreased, isNowDecreased) -> row.pseudoClassStateChanged(DECREASED_PC, isNowDecreased)
);
return row;
});
Here is the complete working example:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.util.Random;
public class StockTrader extends Application {
public static final int STOCK_NAME_LENGTH = 3;
public static final PseudoClass INCREASED_PC = PseudoClass.getPseudoClass("recently-increased");
public static final PseudoClass DECREASED_PC = PseudoClass.getPseudoClass("recently-decreased");
private final Random rng = new Random();
private final ObservableList<Stock> stocks = FXCollections.observableArrayList();
private void startTrading() {
Thread stockTrader = new Thread(() -> {
// A stock trader is an object that sleeps a lot
// and occasionally randomly changes the value of a stock
while(true) {
try {
Thread.sleep(rng.nextInt(500));
} catch (InterruptedException exc) {
// ignored: this thread cannot be interrupted
}
Stock randomStock = stocks.get(rng.nextInt(stocks.size()));
double randomPercentage = rng.nextDouble() * 5 - 2.5;
randomStock.adjustPrice(randomPercentage);
}
});
stockTrader.setDaemon(true);
stockTrader.start();
}
@Override
public void start(Stage stage) {
TableView<Stock> stockTable = new TableView<>();
TableColumn<Stock, String> stockCol = new TableColumn<>("Stock");
stockCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
stockTable.getColumns().add(stockCol);
TableColumn<Stock, Number> priceCol = new TableColumn<>("Price");
priceCol.setCellValueFactory(cellData -> cellData.getValue().priceProperty());
priceCol.setCellFactory(tc -> new TableCell<>(){
@Override
protected void updateItem(Number price, boolean empty) {
super.updateItem(price, empty);
if (empty || price == null) {
setText(null);
} else {
setText(String.format("$%.2f", price.doubleValue()));
}
}
});
stockTable.getColumns().add(priceCol);
// Add 50 random stocks to the table:
for (int i = 0 ; i < 50 ; i++) {
stocks.add(new Stock(randomStockName(), rng.nextDouble() * 100));
}
stockTable.setItems(stocks);
stockTable.setRowFactory(tv -> {
TableRow<Stock> row = new TableRow<>();
row.itemProperty()
.flatMap(Stock::recentlyIncreasedProperty)
.orElse(false)
.addListener(
(obs, wasIncreased, isNowIncreased) -> row.pseudoClassStateChanged(INCREASED_PC, isNowIncreased)
);
row.itemProperty()
.flatMap(Stock::recentlyDecreasedProperty)
.orElse(false)
.addListener(
(obs, wasDecreased, isNowDecreased) -> row.pseudoClassStateChanged(DECREASED_PC, isNowDecreased)
);
return row;
});
BorderPane root = new BorderPane(stockTable);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
startTrading();
}
private String randomStockName() {
StringBuilder name = new StringBuilder();
for (int i = 0 ; i < STOCK_NAME_LENGTH; i++) {
name.append((char) ('A' + rng.nextInt(26)));
}
return name.toString();
}
public static void main(String[] args) {
launch();
}
}