Based on the work by Tomas Mikula's excellent ReactFX project (https://github.com/TomasMikula/ReactFX) and used with permission.
This library allows streaming values generated by properties or changes triggered by JavaFX events with an expressive fluent API which also enables easier management of listeners to prevent memory leaks.
Streams can be used to listen to property changes and take an action each time the property changes. A simple example below shows how to print the value of a button's text property to the console each time it changes:
Changes.of(button.textProperty())
.subscribe(System.out::println);
A more sophisticated example may want to convert the text to upper case and replace a
null
value with an empty string:
Changes.of(button.textProperty())
.map(String::toUpperCase)
.orElse("")
.subscribe(System.out::println);
To also print the initial value of the text property another print statement could be included, but alternatively a value stream can be used. A property of a value stream is that it will send the current value immediately to new subscribers:
Values.of(new SimpleStringProperty("Hello World"))
.subscribe(System.out::println); // prints the value immediately
In the earlier examples null
was not explicitly handled in the map
function. This is
because these functions are null-safe. The functions are not called when the value emitted
is null
. In order to handle null
s, streams offer similar functions to Java's Optional
to deal with the case when null
is emitted.
These functions make dealing with null
a lot simpler as not every map
, flatMap
or
filter
step specifically needs to deal with it. For example, when creating a stream with
several chained properties, the null
checks can be omitted:
Values.of(button.sceneProperty())
.flatMap(scene -> Values.of(scene.windowProperty())) // scene won't be null :)
.flatMap(window -> Values.of(window.showingProperty()))
.orElse(false) // deals with the case when either scene or window is null
.subscribe(showing -> System.out.println("showing is: " + showing));
This library offers three types of streams, each with distinct characteristics. Their
differences lie in how they treat null
and what happens when they are subscribed. See
the summary in the table below:
Type | Null Values | Upon Subscription |
---|---|---|
Event Stream | Discarded | Sends nothing |
Change Stream | Allowed | Sends nothing |
Value Stream | Allowed | Sends current value |
Event streams are the most basic type of stream. When an event occurs, this stream emits a
value, which can be mapped and filtered as needed. Event streams never emit null
, and
mapping an event to null
will discard the event. Event streams only deliver events when
they occur, thus if after subscribing no events occur nothing will be delivered.
Change streams are event streams which allow null values and mapping to null values.
Its methods are null safe, and a separate set of methods is available to deal with null
cases, similar to Java's Optional
. This means that methods like filter
, map
and
flatMap
never have to deal with their input being null
, instead use methods like
orElse
and orElseGet
. Subscribers can still receive null
, unlike event streams.
Value streams expand further upon change streams with a notion of a current value which new subscribers will immediately receive as their first value. This makes value streams very suitable as a binding source for a property as they immediately emit their current state.
The different types of streams can be converted into one or the other with a few specific methods. Usually these methods could violate an invariant of the current stream type but allow this by returning a different stream type for which this is allowed.
For example, filtering a value stream may result in the stream not emitting a value upon
subscription making it unsuitable as a binding. The filter
method therefore returns a
change stream instead. Another example is changing a change stream into a value stream
with withDefault
. This method assigns a default value to the stream which can be emitted
immediately upon subscription.
The table below shows which of the most commonly used functions are available for each stream type and whether the type of stream changes as a result:
Function | Event | Change | Value |
---|---|---|---|
map, flatMap | X | X | X |
filter | X | X | X(C) |
peek | X | X | X |
withDefault, withDefaultGet | X(V) | X(V) | - |
orElse, orElseGet | - | X | X |
or | - | X | X |
conditionOn | X | X | X |
flatMapToChange | - | - | X(C) |
The following table shows which terminal operations are available for each stream type:
Function | Event | Change | Value |
---|---|---|---|
subscribe | X | X | X |
toBinding | - | - | X |
Streams only observe their source when a consumer is currently subscribed.
ValueStream<String> vs = Values.of(button.textProperty())
.map(String::toUpperCase);
In the above example, the button's text property is not observed until an actual subscriber is added to the stream:
vs.subscribe(System.out::println);
Streams of this type are called lazy streams. All streams provided by this package are lazy and will only observe their source when needed.
As lazy streams will stop observing their source when they have no subscribers, the source stream will not prevent garbage collection when there are no more subscribers. There is therefore no need to use weak listeners for observing the source stream.
An advantage of this approach is that an example like below will function as one would
expect, and will keep printing changes in button.textProperty()
:
Values.of(button.textProperty())
.map(t -> t + "World")
.subscribe(System.out::println);
Contrast this with JavaFX's standard binding mechanism which may garbage collect the binding at any time because of its use of weak listeners:
button.textProperty()
.concat("World") // weak binding used here
.addListener((obs, old, current) -> System.out.println(current));
This can be very surprising, especially when adding the concat
function at a later stage,
because that simple change will result in a completely different runtime behavior.
This project was created in the hope to add additional functionality directly to JavaFX to address a few of its rough edges. Mainly:
- Type-safe
Bindings#select
functionality, which allows to create bindings to nested properties. The current implementation is not type safe and does not offer much flexibility to customize the binding.
The project purposely contains only a small well defined subset of code adapted from ReactFX with the goal
of making the project easier to evaluate for potential inclusion into JavaFX. Direct inclusion would
offer major advantages by adding default methods to the ObservableValue
and Binding
interfaces making
classes that implement these interfaces act more like Optional
's would:
Binding<String> quotedTitleText = model.titleProperty()
.map(text -> "'" + text "'"); // new `map` method on `Binding`
In standard JavaFX, creating a binding to a nested property is a cumbersome affair. One has to keep track of the listeners to unregister them when a parent property changes, and reregister the listener on the new value. With multiple levels of nesting this can quickly become complicated and error prone.
An example from JavaFX itself is the implementation of the treeShowing
property. It tracks whether
or not a Node
is currently showing on the screen. In order to do this, it must check if the Node
has a Scene
, whether the Scene
has a Window
, and whether that Window
is currently shown:
ChangeListener<Boolean> windowShowingChangedListener = (win, oldVal, newVal) -> updateTreeShowing();
ChangeListener<Window> sceneWindowChangedListener = (scene, oldWindow, newWindow) -> {
if (oldWindow != null) {
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
}
if (newWindow != null) {
newWindow.showingProperty().addListener(windowShowingChangedListener);
}
updateTreeShowing();
};
ChangeListener<Scene> sceneChangedListener = (node, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
Window window = oldScene.windowProperty().get();
if (window != null) {
window.showingProperty().removeListener(windowShowingChangedListener);
}
}
if (newScene != null) {
newScene.windowProperty().addListener(sceneWindowChangedListener);
Window window = newScene.windowProperty().get();
if (window != null) {
window.showingProperty().addListener(windowShowingChangedListener);
}
}
updateTreeShowing();
};
This can already be expressed much more succintly by using the Bindings#select
function:
BooleanProperty treeShowing = Bindings.selectBoolean(node.sceneProperty(), "window", "showing");
The method however is not type safe. A mistake in one of the string parameters or the choice of select
method will lead to errors at runtime. It will also complain about null
values and map them to some
standard value.
With streams we can create the same binding in a type-safe manner:
Binding<Boolean> treeShowing = Values.of(node.sceneProperty())
.flatMap(s -> Values.of(s.windowProperty()))
.flatMap(w -> Values.of(w.showingProperty()))
.orElse(false)
.toBinding();
This is far less cumbersome and still 100% type safe.
When you bind a property in JavaFX you have to carefully consider the lifecycle of the two properties
involved. Calling bind
on a property will keep a target property synced with a source property.
target.bind(source); // keep target in sync with source
This is equivalent to adding a (weak) listener (weak listener code omitted here) and keeping track of the property target was bound to:
source.addListener((obs, old, current) -> target.set(current));
target.getProperties().put("boundTo", source);
In both these cases:
- source refers to target through the listener added because it needs to update the target when it changes
- target refers to source as the property it is "bound to" in order for
unbind
to do its magic
In JavaFX, bind
will use a weak listener, which means that the target can be garbage collected independently
from the source property. However, the reference from target to source with its "bound to" property is a hard
reference (if it were weak then a binding could stop working without notice because the source could be
garbage collected and stop sending its updates). This means that the lifecycle of the source property is
now closely tied to the target property.
If the target property is a long-lived object (like a data model) and the source property is a shorter lived object like a UI element, you could have inadvertently created a big memory leak; all UI components have a parent and a scene property, so effectively keeping a reference to any UI element can keep an entire scene or window from being garbage collected.
Something as simple as keeping the selection of a ListView
in sync with a model can lead to this:
model.selectedItemProperty()
.addListener((obs, old, current) -> listView.getSelectionModel().select(current));
Assuming model
here is a long-lived object that perhaps is re-used next time the UI is shown to remember
the last selected item, the listener as shown above will prevent the ListView
and all other UI components
that it refers to from being garbage collected.
To prevent this one must remember to wrap the above listener in a WeakChangeListener
and be careful not
to keep a reference around to the unwrapped change listener. Using a weak listener is not a perfect
solution however. The listener only stops working when a garbage collection cycle runs and in the mean
time the UI code may interfere with normal operations if the selected item is changed in the model, as it
could trigger code in the (soon to be garbage collected) UI, which may still trigger other changes.
It might be better to disable the listener as soon as the UI is hidden. Doing this manually means keeping
track of the listeners involved that may need unregistering (and potentially reregistering if the UI becomes
visible again). Instead we could listen to a property that tracks the showing status of our UI.
Unfortunately, this is somewhat involved as there is no easy property one can listen to; you have to listen
for Scene
changes, check which Window
it is associated with and then bind to Window::showingProperty
-- and update these listeners if the scene or window changes.
With Streams one could safely bind a UI property to a model only when the UI is visible:
model.selectedItemProperty()
.conditionOn(isShowing)
.subscribe(selectedItem -> listView.getSelectionModel().select(selectedItem));
Where the isShowing
variable can be created like this:
Binding<Boolean> isShowing = Values.of(listView.sceneProperty())
.flatMap(s -> Values.of(s.windowProperty()))
.flatMap(w -> Values.of(w.showingProperty()))
.orElse(false)
.toBinding();
Or with a small helper class, which only needs the Node
involved as a parameter:
model.selectedItemProperty()
.conditionOn(Helper.isShowing(listView))
.subscribe(selectedItem -> listView.getSelectionModel().select(selectedItem));
The above binding to model.selectedItemProperty()
will only be present while listView
is
showing. If the list view is hidden, the listener is unregistered, and if it is shown again
the listener is re-added. If the UI is hidden, it will instantly stop reacting to any
changes in the model and (if also no longer referenced) will eventually be garbage collected.