Proof of concept for fluent bindings for ObservableValue

John Hendrikx hjohn at xs4all.nl
Wed Mar 24 21:49:16 UTC 2021


I just wanted to draw some attention to a recent proof of concept I made 
in this pull request: https://github.com/openjdk/jfx/pull/434

It is based on the work I did in 
https://github.com/hjohn/hs.jfx.eventstream which is in part based on 
work done in ReactFX by Tomas Mikula. The PR itself however shares no 
code with ReactFX and is
completely written by me.

If there is interest, I'm willing to invest more time in smoothing out 
the API and documentation, investigating further how this would interact 
with the primitive types and adding unit test coverage (I have extensive 
tests, but thesea are written in JUnit 5, so they would require 
conversion or JavaFX could move to support JUnit 5).

What follows below is the text of the PR for easy reading. Feedback is 
appreciated.

================

This is a proof of concept of how fluent bindings could be introduced to 
JavaFX. The main benefit of fluent bindings are ease of use, type safety 
and less surprises. Features:

Flexible Mappings
Map the contents of a property any way you like with map, or map nested 
properties with flatMap.

Lazy
The bindings created are lazy, which means they are always invalid when 
not themselves observed. This allows for easier garbage collection (once 
the last observer is removed, a chain of bindings will stop observing 
their parents) and less listener management when dealing with nested 
properties. Furthermore, this allows inclusion of such bindings in 
classes such as Node without listeners being created when the binding 
itself is not used (this would allow for the inclusion of a 
treeShowingProperty in Node without creating excessive listeners, see 
this fix I did in an earlier PR: #185)

Null Safe
The map and flatMap methods are skipped, similar to java.util.Optional 
when the value they would be mapping is null. This makes mapping nested 
properties with flatMap trivial as the null case does not need to be 
taken into account in a chain like this: 
node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::showingProperty). 
Instead a default can be provided with orElse or orElseGet.

Conditional Bindings
Bindings can be made conditional using the conditionOn method. A 
conditional binding retains its last value when its condition is false. 
Conditional bindings donot observe their source when the condition is 
false, allowing developers to automatically stop listening to properties 
when a certain condition is met. A major use of this feature is to have 
UI components that need to keep models updated which may outlive the UI 
conditionally update the long lived model only when the UI is showing.

Some examples:

void mapProperty() {
   // Standard JavaFX:
   label.textProperty().bind(Bindings.createStringBinding(() -> 
text.getValueSafe().toUpperCase(), text));

   // Fluent: much more compact, no need to handle null
   label.textProperty().bind(text.map(String::toUpperCase));
}

void calculateCharactersLeft() {
   // Standard JavaFX:
 
label.textProperty().bind(text.length().negate().add(100).asString().concat(" 
characters left"));

   // Fluent: slightly more compact and more clear (no negate needed)
   label.textProperty().bind(text.orElse("").map(v -> 100 - v.length() + 
" characters left"));
}

void mapNestedValue() {
   // Standard JavaFX:
   label.textProperty().bind(Bindings.createStringBinding(
     () -> employee.get() == null ? ""
         : employee.get().getCompany() == null ? ""
         : employee.get().getCompany().getName(),
     employee
   ));

   // Fluent: no need to handle nulls everywhere
   label.textProperty().bind(
     employee.map(Employee::getCompany)
             .map(Company::getName)
             .orElse("")
   );
}

void mapNestedProperty() {
   // Standard JavaFX:
   label.textProperty().bind(
     Bindings.when(Bindings.selectBoolean(label.sceneProperty(), 
"window", "showing"))
       .then("Visible")
       .otherwise("Not Visible")
   );

   // Fluent: type safe
   label.textProperty().bind(label.sceneProperty()
     .flatMap(Scene::windowProperty)
     .flatMap(Window::showingProperty)
     .orElse(false)
     .map(showing -> showing ? "Visible" : "Not Visible")
   );
}

void updateLongLivedModelWhileAvoidingMemoryLeaks() {
   // Standard JavaFX: naive, memory leak; UI won't get garbage collected
   listView.getSelectionModel().selectedItemProperty().addListener(
     (obs, old, current) -> 
longLivedModel.lastSelectedProperty().set(current)
   );

   // Standard JavaFX: no leak, but stops updating after a while
   listView.getSelectionModel().selectedItemProperty().addListener(
     new WeakChangeListener<>(
       (obs, old, current) -> 
longLivedModel.lastSelectedProperty().set(current)
     )
   );

   // Standard JavaFX: fixed version
   listenerReference = (obs, old, current) -> 
longLivedModel.lastSelectedProperty().set(current);

   listView.getSelectionModel().selectedItemProperty().addListener(
     new WeakChangeListener<>(listenerReference)
   );

   // Fluent: naive, memory leak... fluent won't solve this...
   listView.getSelectionModel().selectedItemProperty()
       .subscribe(longLivedModel.lastSelectedProperty()::set);

   // Fluent: conditional update when control visible

   // Create a property which is only true when the UI is visible:
   ObservableValue<Boolean> showing = listView.sceneProperty()
       .flatMap(Scene::windowProperty)
       .flatMap(Window::showingProperty)
       .orElse(false);

   // Use showing property to automatically disconnect long lived model
   // allowing garbage collection of the UI:
   listView.getSelectionModel().selectedItemProperty()
     .conditionOn(showing)
     .subscribe(longLivedModel.lastSelectedProperty()::set);

   // Note that the 'showing' property can be provided in multiple ways:
   // - create manually (can be re-used for multiple bindings though)
   // - create with a helper: Nodes.showing(Node node) -> 
ObservableValue<Boolean>
   // - make it part of the Node class; as the fluent bindings only bind 
themselves
   //   to their source when needed (lazy binding), this won't create 
overhead
   //   for each node in the scene
}
Note that this is based on ideas in ReactFX and my own experiments in 
https://github.com/hjohn/hs.jfx.eventstream. I've come to the conclusion 
that this is much better directly integrated into JavaFX, and I'm hoping 
this proof of concept will be able to move such an effort forward.

--John


More information about the openjfx-dev mailing list