Introduction
I was working on a JFX 21 project last week and IntelliJ came up with ObservableValue.map()
as a suggestion. I looked it up in the JavaDocs, I tried it, it worked, and I moved on with what I was doing.
For the next few days, however, some thoughts gnawed at my brain: “How had I missed this?”, and “Isn’t this is a game-changer?”.
How had I missed this?
I went back and looked, and right at the bottom of the entry… “Since 19”.
Then I looked at the JavaDocs page for ObservableValue
in both JFX 16 and 21. There are just 3 methods in JFX 16: addListener()
, getValue()
, and removeListener()
. There are 9 methods in the JFX 21 version. Those 6 new methods (plus one in Observable
) are what we are going to look at in this article.
And yes, these are game changers!
ObservableValue.map() and ObservableValue.orElse()
Let’s start by looking at the JavaDocs for ObservableValue.map():
Returns an ObservableValue that holds the result of applying the given mapping function on this value. The result is updated when this ObservableValue changes. If this value is null, no mapping is applied and the resulting value is also null.
Hang on! This pretty much describes, in a general way, what every function in the Bindings
library does - at least when they are dependant on only one Observable
.
Let’s look at an example. First, the old Bindings
library way:
region.minWidthProperty().bind(Bindings.createDoubleBinding({if (someBooleanProperty.get()) 50.0 else 75.0}, someBooleanProperty))
Here, we are just using a BooleanProperty
to control the minWidth
of some region in our layout. If the BooleanProperty
holds true
then we’ll set the minWidth
to 50.0 otherwise it will be 75.0. Whenever the value in someBooleanProperty
changes, then region.minWidthProperty()
will change in lock-step.
Now, with ObservableValue.map()
:
region.minWidthProperty().bind(someBooleanProperty.map{if (it) 50.0 else 75.0})
This does exactly the same thing. Note that ObservableValue.map()
returns an ObservableValue
wrapping the type defined by the Function
passed to it. However, just like with the Bindings
library methods, that Function
deals with the value inside the ObservableValue
and returns a non-observable type. In this example, it
is Boolean
and the Function
returns Double
. The function is Function<Boolean, Double>
.
Clearly this new method is less code, and easier to understand. It’s also easy to view it as an extension of the Fluent API. You can also chain Observable.map()
calls together if it makes it more clear.
Another benefit is that it’s the same function and format for every kind of conversion that you might want to do. Instead of having to pick the correct method from Bindings
, just use map and let the compiler infer the types and off you go.
There’s one hitch…
Remember this from the JavaDocs, quoted above?
If this value is null, no mapping is applied and the resulting value is also null.
Uh oh! Any ObservableValue
can potentially hold a Null
value, in which case we’ll get a Null
back out if it. In our example, do we really want Null
if someBooleanProperty
holds a Null
value? Probably not.
Here comes orElse()
to the rescue:
region.minWidthProperty().bind(someBooleanProperty.map{if (it) 50.0 else 75.0}.orElse(60.0))
Once again, this will return an ObservableValue
of the right type, but the parameter passed to orElse()
is a value of that type, not an ObservableValue
.
You’ll need to keep in mind that since ObservableValue.map()
returns an ObservableValue
and not a Property
of some kind, it cannot be used for a bidirectional binding.
ObservableValue.flatMap()
My first reaction when seeing this was, “Ho hum, just another map
function for completeness.”
But then I read the example in the JavaDocs, which starts with this:
ObservableValue<Boolean> isShowing = listView.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
.orElse(false);
and the explanation:
Changes in any of the values of: the scene of listView, the window of that scene, or the showing of that window, will update the boolean value isShowing.
This is big!
One of the biggest headaches in JavaFX is handling nested Properties
, or Properties
of Properties
. This pops most often when you’re dealing with TableView
and ListView
, or really any other container based on VirtualFlow
. That’s because the cells have a Property
called item
and it contains the value currently held in the cell. When that value is a simple object or primitive, like a String
then it’s easy simply bind some Node
in the cell layout to item
and it will work.
But when item
is an object composed of other Observables
then it becomes harder to do this. Binding, say, label.textProperty()
to item.nameProperty()
is problematic because of the reach-through to the component field. When item
changes, the binding goes with it, and now label.textProperty()
is bound to the nameProperty()
of some prior value of item
and not the current one.
This was particularly irksome with ListView
where the items
passed to the ListCells
are often complex objects composed of Observable
fields.
There was a way around this, and that was Bindings.select()
. It was problematic, though, because it used reflection and depended on the names used for the component fields following a pattern.
ObservableValue.flatMap()
fixes that. As a matter of fact, the current JavaDocs for Bindings.select()
tell you to use Observable.flatMap()
. This is somewhat analogous to how PropertyValueFactory
should be replaced with lambda calls after Java 8.
ObservableValue.flatMap() Example
Let’s take a look at how this works. One of the most common uses for nested Properties
is to display information from the selected item in a TableView
. This is usually done by linking through the SelectedItemProperty
of a TableView
, which, in turn, is normally composed of other Properties
.
Here’s a simple example of how this works with ObservableValue.flatMap()
class FlatMap : Application() {
val selectedItem: ObjectProperty<TableData> = SimpleObjectProperty()
override fun start(stage: Stage) {
val scene = Scene(createContent(), 620.0, 300.0).apply {
addStyleSheet("/ca/pragmaticcoding/widgetsfx/css/LabelBox.css")
addWidgetStyles()
}
stage.title = "LabelBox Demo"
stage.scene = scene
stage.show()
}
fun createContent() = BorderPane().apply {
padding = Insets(20.0, 20.0, 20.0, 20.0)
styleClass += "wrapper-region"
center = createTable()
right = createPane()
}
private fun createTable(): Region = TableView<TableData>().apply {
columns += TableColumn<TableData, String>("Name").apply {
cellValueFactory = Callback { it.value.nameProperty }
}
columns += TableColumn<TableData, String>("Address").apply {
cellValueFactory = Callback { it.value.addressProperty }
}
items += listOf(
TableData("Wizard of Oz", "1 Yellow Brick Road"),
TableData("Herman Munster", "1313 Mockingbird Lane"),
TableData("Norman Bates", "Bates Motel")
)
selectedItem.bind(selectionModel.selectedItemProperty())
columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY_LAST_COLUMN
maxWidth = 300.0
}
private fun createPane(): Region = VBox(10.0).apply {
children += HBox(10.0, Label("Name: "), Label().apply {
textProperty().bind(selectedItem.flatMap { it.nameProperty })
})
children += HBox(10.0, Label("Address: "), Label().apply {
textProperty().bind(selectedItem.flatMap { it.addressProperty })
})
minWidth = 300.0
padding = Insets(15.0)
}
}
class TableData(val name: String, val address: String) {
val nameProperty: StringProperty = SimpleStringProperty(name)
val addressProperty: StringProperty = SimpleStringProperty(address)
}
fun main() {
Application.launch(MapsAndSubscriptions::class.java)
}
Our Model
for the TableView
is TableData
, which is a object composed of two StringPropertie
, nameProperty
and addressProperty
. The TableView
just has two columns, one for “Name” and one for “Address”. Off to the right of the TableView
we have a VBox
with two Label
pairs in HBoxes
. One for the name, and one for the address.
There’s an ObjectProperty<TableData>
field in our application. It’s bound to the TableView's selectedItemProperty
, so it will always reflect the currently selected item in the TableView
.
Two of the Labels
on the right side need to bound to the corresponding Properties
in value held in selectedItem
, and this is done via flatMap()
:
textProperty().bind(selectedItem.flatMap { it.nameProperty })
and:
textProperty().bind(selectedItem.flatMap { it.addressProperty })
Now, when either selectedItem
changes, or either of the two Properties
that make up that current value changes, the Labels
will also change.
When we run this, we first see:
Then, when we select an item:
And, then another:
The one thing that you cannot do with this is to change those data Labels
to TextField
and bidirectionally bind their TextProperty
to the flatMap()
value, because it returns ObservableValue
and not a Property
of some sort.
ObservableValue.subscribe() and Observable.subscribe()
Subscription
is another neat new feature, this one added in JFX 21. From the description in the issue, I can paraphrase the situation that they were trying to solve:
Listeners
are heavyweight and contain features that application programmers seldom use.ChangeListener
is often used whenInvalidationListener
would suffice, and the references toObservableValue
and theoldValue
are seldom seen in application code. Consequently,Listeners
are cumbersome to implement in application code. The inclusion ofLambdas
andMethod references
to the language make it increasingly difficult to removeListeners
fromObservables
.
This has been my observation when looking at other programmers’ projects also. ChangeListener
is used almost exclusively, even though InvalidationListener
is more appropriate in many cases. At times, the Listener
is only used to trigger something which doesn’t even need the new value of the Observable
.
Personally, I now rarely work on the kind of projects where garbage collection is an issue, and defunct Listener
references to Observables
prevent those Observables
from being garbage collected, but I can see the need to be able to remove Listeners
without bloating the code base with boilerplate to achieve this.
The Differences between InvalidationListener and ChangeListener
Before we go on, it’s best to have a quick review about how the two Listener
types in JavaFX work.
The first concept to understand is “invalidation”. An Observable
is considered to have a “valid” value when it’s, well, valid - or known to be correct. If an Observable
is bound to another Observable
in some way, and that other Observable
changes, then it’s possible that the value of the bound Observable
has also changed. Under the hood, whenever an Observable
is changed, JavaFX gets a list of all the other Observables
that are bound to it and changes their status from “valid”, to “invalid” (a process called “invalidation”). Additionally, when an Observable
is invalidated, JavaFX gets a list of all of the other Observables
that are bound to it and invalidates them, too.
Finally, Observables
are flipped back to “valid” when get()
or getValue()
are called on them.
An InvalidationListener
triggers when an Observable
has been invalidated. It doesn’t mean that the value has changed, just that it might have changed. Consider the following code:
val a : IntegerProperty = SimpleIntegerProperty(10)
val b : BooleanProperty = a.map{if (it > 20) true else false}
b.addListener(InvalidationListener{println("I was triggered ${b.value}")})
a.value = a.value + 1
a.value = a.value + 20
After the second line, the value of a
was 10 and b
was false, b
was valid. After the first increment of a
, the value of a
was 11, and b was invalidated. This would trigger the Listener
and, “I was triggered false” would print. After this printed, b
would be valid again. After the last line a
was 31 and b was invalidated again. This would trigger the Listener
and “I was triggered true” would print. And then, once again, b
would be valid.
However, if we take out the ${b.value}
from the println
, so that the code looks like this:
val a : IntegerProperty = SimpleIntegerProperty(10)
val b : BooleanProperty = a.map{if (it > 20) true else false}
b.addListener(InvalidationListener{println("I was triggered")})
a.value = a.value + 1
a.value = a.value + 20
Things are different.
After the second line, the value of a
was 10 and b
was false, b
was valid. After the first increment of a
, the value of a
was 11, and b was invalidated. This would trigger the Listener
and, “I was triggered” would print. After this printed, b
would still be invalid. After the last line a
was 31 and b
was already invalid. This would not trigger the Listener
and nothing would print. And then, at the end, b
would still be invalid.
The best mental model of a ChangeListener
is to think of it as a wrapper around an InvalidationListener
, with a place to hold the last value stored in the Observable
. When the InvalidationListener
triggers, it invokes the Observable.get()
method, revalidating the Observable
, and comparing the results to the previous value that it has stored. If these two values are different, then it triggers the ChangeListener
action.
The key differences between InvalidationListener
and ChangeListener
are that ChangeListener
only triggers the action on an actual change of the value, and it always re-validates the Observable
automatically, even if the invalidation didn’t result in a value change. Beyond that, ChangeListener
gives you old and new values, if your logic needs to compare them.
With that in mind, let’s look at the new subscription facility in JavaFX…
Using Subscriptions
This feature introduces a new Interface
called Subscription
which allows you to track and combine the subscriptions you’ve added to your Observables
. Subscriptions
are returned from the subscribe()
methods. The Subscription
interface has methods that allow you to combine Subscriptions
and to unsubscribe Subscriptions
.
All of that sounds very circular. Subscription
doesn’t really talk about what they do, just how to combine them and get rid of them. The idea is that you can create a compound Subscription
that acts as a container for all of your individual Subscriptions
so that when you are done with them you can unsubscribe them all at once. And it’s all easy, easy, easy to do.
There are three subscription()
methods; two in ObservableValue
and one in Observable
. They differ in only the argument that they take. Here’s what they do when triggered:
- ObservableValue.subscribe(Consumer)
Will invoke aConsumer
that accepts the new value of theObservableValue
. - ObservableValue.subscribe(BiConsumer)
Will invoke aBiConsumer
that accepts the old value and the new value of theObservableValue
. - Observable.subscribe(Runnable)
Will invoke aRunnable
that does accept not any values.
You have to read the JavaDocs fairly carefully to get all the nuances in here. Let’s experiment…
Using Observable.subscribe()
Let’s look at the first version, which just takes a runnable as its argument. How does this interact with the Invalidation status of the Observable
.
Here’s some test code:
class MapsAndSubscriptions : Application() {
override fun start(stage: Stage) {
val scene = Scene(createContent(), 420.0, 300.0).apply {
addStyleSheet("/ca/pragmaticcoding/widgetsfx/css/LabelBox.css")
addWidgetStyles()
}
stage.title = "LabelBox Demo"
stage.scene = scene
stage.show()
}
fun createContent() = BorderPane().apply {
padding = Insets(20.0, 20.0, 20.0, 20.0)
styleClass += "wrapper-region"
center = VBox(20.0, createPane())
}
private fun createPane(): Region = VBox(10.0).apply {
val counter: IntegerProperty = SimpleIntegerProperty(0)
val textArea = TextArea()
counter.addListener(InvalidationListener { textArea.text += "Invalidation Listener triggered\n" })
counter.subscribe(Runnable { textArea.text += "Subscription triggered\n" })
children += Button("Click Me").apply {
val buttonCounter: IntegerProperty = SimpleIntegerProperty(0)
textProperty().bind(buttonCounter.map { "Click Me $it" })
onAction = EventHandler {
textArea.text += "Button clicked ${buttonCounter.value}\n"
counter.value = buttonCounter.value++
}
}
children += textArea
}
}
fun main() {
Application.launch(MapsAndSubscriptions::class.java)
}
This is going to give us a window with a Button
and a TextArea
. Every time the Button
is clicked, it’s going to add some text to the TextArea
and update an IntegerProperty
called counter
. In addition, it will increment another IntegerProperty
called buttonCounter
which is displayed on the Button
through a map()
.
Additionally, there is an InvalidationListener
on counter
that does not revalidate counter
by calling its get()
method. Nor does the Subscription
.
Let’s see what happens when you click the Button
a few times:
On the first click, buttonCounter
is 0 and it sets counter
to 0, which is what it already was. So this does not invalidate counter
.
On the second click, buttonCounter
is 1 and it sets counter
to 1, which then invalidates counter
. Both the InvalidationListener
and the Subscription
are triggered.
On the subsequent clicks, counter
is incremented, but it’s already been invalidated so neither the InvalidationListener
nor the Subscription
are triggered again.
Now, let’s change one line of code; the line that creates the InvalidationListener
:
counter.addListener(InvalidationListener { textArea.text += "Invalidation Listener triggered ${counter.value}\n" })
This calls counter.get()
through the Kotlin property accessor value
. So this will revalidate counter
. Here’s the output:
Finally, we put the InvalidationListener
code back the way it started, and change the Subscription
line so that it will revalidate counter
:
counter.subscribe(Runnable { textArea.text += "Subscription triggered ${counter.value}\n" })
From this, I think it’s safe to assume that Observable.subscribe()
is analogous to adding an InvalidationListener
to your Observable
. This is in agreement with the JavaDocs, which state:
The provided subscriber is akin to an InvalidationListener without the Observable parameter.
ObservableValue.subscribe()
Now let’s look at the two methods defined in ObservableValue
. These are much more like ChangeListener
.
With a Consumer
The next version of subcribe()
that we’ll look at is the one that takes a Consumer
. We’ll leave all the rest of the example code alone, including the InvalidationListener,
but change the subscription:
counter.subscribe(Consumer { textArea.text += "Subscription triggered \n" })
Note that we’re not accessing the value passed to the Consumer
here. This is what we get:
This is almost what you’d expect. Nothing happens after the first Button
click because the value doesn’t change, it stays 0. After that, the Subscription
, in getting the value to pass to the Consumer
is revalidating our Property
. So we expect to see both the Subscription
and the InvalidationListener
trigger on each subsequent Button
click.
But the very first “Subscription Triggered” message is interesting. It clearly happens when the Subscription
is created. Let’s check the JavaDocs on that:
Creates a Subscription on this ObservableValue which immediately provides the current value to the given valueSubscriber, followed by any subsequent values whenever its value changes. The valueSubscriber is called immediately for convenience, since usually the user will want to initialize a value and then update on changes.
Ah! When I first read this it wasn’t clear to me what “immediately provides” meant. Now it’s clear, as is the explanation about initializing a value as a use case.
Aside from this, it’s safe to consider this version of subscribe()
to be about the same as a ChangeListener
that only passes the newVal
to its Handler
.
With a BiConsumer
Now we’ll swap out the Subscription
to use the last version, the one that passes both the old and the new values to a BiConsumer
:
counter.subscribe { _: Number, _: Number -> textArea.text += "Subscription triggered \n" }
This is the Kotlin version of a BiConsumer. We’re not going to use the values passed in, so _
is used a a placeholder.
The output looks like this:
This is also pretty much what you’d expect, except that we don’t get the initial firing of the Subscription
when it’s created. Check the JavaDocs and you’ll see that there is no mention of “immediately provides” for this version of subscribe()
.
Invalidation Without a Value Change
We know from the discussion above, that an Observable
can invalidate because something that it’s bound to has been invalidated, while its own value doesn’t actually change. Let’s see how that works with Subscriptions
. We’re going to add a new Observable
to our code, that is bound to counter
through a map that means it wont always change when
counter` changes.
val counter: IntegerProperty = SimpleIntegerProperty(0)
val boundCounter = counter.map { if ((it as Int) < 3) 10 else 20 }
boundCounter.addListener(InvalidationListener { textArea.text += "Invalidation Listener triggered\n" })
boundCounter.subscribe { _: Number, _: Number -> textArea.text += "Subscription triggered \n" }
All the rest of the code is the same. Here’s the output:
This is what I would have expected (hoped?) would happen. For the second and third button clicks, counter
changes and invalidates which, in turn, invalidates boundCounter
. At the same time, the Subscription
, which doesn’t trigger the BiConsumer, does re-validate the Observables
because it needs to call get()
on boundCounter
in order to decide if it has changed.
We’ll try the same thing, but with the Consumer
version:
This is exactly the same, except that we have that initial triggering when the Subscription
is created.
Which to Use?
I think that for 95% of use cases - which is when you really want to trigger when the value actually changes, but you don’t care what the previous value was - then you should just use the ObservableValue.subscribe(Consumer)
version. Use this even if you don’t want to use the new value in anything directly. This is going to free you up from having to worry about invalidation issues. On those few cases that you do need the previous value, then use the BiConsumer
version.
I would reserve the Runnable
version for those rare cases when you really do care about the difference between invalidation and change, and you want to fire whenever anything in your chain of bound Properties
changes.
The other big thing to think about is this initial firing of the Consumer
version when it’s initiated. I’ve seen applications where a Property
is used to connect to modules of a together, and it’s passed from the first module to the second via a constructor parameter. Then the dependent module uses a ChangeListener
to do something when the controlling module changes the value of the Property
. In that situation, you usually want to run whatever code executes when the value changes when you first get the property, because often it already has a valid value in it. So you’re forced to put the ChangeListener
code into a method so that you can manually force it to run from the constructor. With this version of Subscription
, you don’t need to do that.
Combining and Unsubscribing
One of the big advantages to Subscriptions
is that it’s easier to remove them than the corresponding Listener
type, especially when the Listener
was declared via a lambda or method reference. This is because the removeListener()
functions take a reference to Listener
to be removed. For instance, in our example code above, we have this:
boundCounter.addListener(InvalidationListener { textArea.text += "Invalidation Listener triggered\n" })
If we want to be able to remove this listener, then we have to do this:
val listener = InvalidationListener { textArea.text += "Invalidation Listener triggered\n" }
boundCounter.addListener(listener)
and then, later on we can do:
boundCounter.removeListener(listener)
While addListener()
has a return type of void
, subscribe()
has a return type of Subscription
. And the Subscription
interface has a method called unsubscribe
. Back to our example code, we can do this:
val subscription = boundCounter.subscribe { _: Number, _: Number -> textArea.text += "Subscription triggered \n" }
Which means that later we can do this:
subscription.unsubscribe()
Now, if you have a number of Subscriptions
and you don’t want to keep track of them individually throughout your code, you can do something like this:
val subscriptions = Subscription.EMPTY
.
.
subscriptions.add(boundCounter.subscribe { _: Number, _: Number -> textArea.text += "Subscription triggered \n" })
.
.
subscriptions.unsubscribe()
alternatively, you can do something like this:
val subscriptions = Subscription.combine(subscription1, subscription2, subscription3)
.
.
subscriptions.unsubscribe()
ObservableValue.when()
Observable.when()
is described primarily as a means to avoid having defunct references which prevent garbage collection of otherwise unused Properties
. But how does it work, and what can we expect to see when we use when()
ObservableValue.when() Example
class WhenApplication : Application() {
override fun start(stage: Stage) {
val scene = Scene(createContent(), 420.0, 300.0).apply {
addStyleSheet("/ca/pragmaticcoding/widgetsfx/css/LabelBox.css")
addWidgetStyles()
}
stage.title = "LabelBox Demo"
stage.scene = scene
stage.show()
}
fun createContent() = BorderPane().apply {
padding = Insets(20.0, 20.0, 20.0, 20.0)
styleClass += "wrapper-region"
center = VBox(20.0, createPane())
}
private fun createPane(): Region = VBox(10.0).apply {
val textArea = TextArea()
val bindingEnabled: BooleanProperty = SimpleBooleanProperty(false)
val counter: IntegerProperty = SimpleIntegerProperty(0)
val boundCounter: IntegerProperty = SimpleIntegerProperty(25)
boundCounter.bind(counter.`when`(bindingEnabled).map { it.toInt() + 17 })
boundCounter.subscribe { it: Number -> textArea.text += "Subscription triggered $it\n" }
bindingEnabled.subscribe { it: Boolean -> textArea.text += "Binding enabled: $it\n" }
children += Button("Click Me").apply {
val buttonCounter: IntegerProperty = SimpleIntegerProperty(0)
textProperty().bind(buttonCounter.map { "Click Me $it" })
onAction = EventHandler {
textArea.text += "Button clicked ${buttonCounter.value}\n"
counter.value = buttonCounter.value++
}
}
children += CheckBox("Binding On").apply {
selectedProperty().bindBidirectional(bindingEnabled)
}
children += HBox(10.0, Label("Bound Counter:"), Label().apply { textProperty().bind(boundCounter.asString()) })
children += textArea
}
}
A quick Kotlin note: when
is a reserved keyword in Kotlin, so to use when()
we need put the “when” in backticks.
Here we have a couple if IntegerProperties
, one called boundCounter
which is bound to the other, called counter
through a map()
and a when()
. The map()
just adds 17 to the value in counter
. There is a BooleanProperty
called bindingEnabled
which is bound bidirectionally to a CheckBox
.
With this we can control the when()
with the CheckBox
and increment counter
via the Button
. Then we can see how it works.
Let’s look at the output from this. First, once the application has been started and the Button has been clicked twice:
We see the output from creating the two Subscriptions
, as they fire immediately. Interestingly, even though the value in bindingEnabled
is false
, and boundCounter
was initialised to 25, it’s shows as 17 when the Subscription
was added. There’s evidence of two Button
clicks, and counter
now has a value of 1 (remember, it’s one behind the number shown on the Button
). Yet the value in boundCounter
remains at 17.
Next, we’ll click on the CheckBox
to active the when()
and click the Button
again. After that, we’ll unselect the CheckBox
:
We see the output from the two subscriptions when the when()
is activated. One is for bindingEnabled
changing to true
, and the other is when boundCounter
is updated because the when()
is activated. Then the Button
is clicked, and we see the subscription on boundCounter
firing, telling us the value in now 19. Finally, we see the bindingEnabled
subscription firing when the CheckBox
was unselected. The current value of boundCounter
remains 19.
Finally, we’ll click the Button
again:
Here we see Button
click message, but nothing else. The value of boundCounter
remains 19.
Conclusion
In my opinion, ObservableValue.map()
, and the Subscription
methods are “game changers” for writing Reactive JavaFX applications. Using these methods will result in cleaner, simpler code with less boilerplate. By themselves, these are reason enough to start using JFX 21 right now.