RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue [v17]

John Hendrikx jhendrikx at openjdk.org
Fri Jul 1 09:04:11 UTC 2022


> This is an implementation of the proposal in https://bugs.openjdk.java.net/browse/JDK-8274771 that me and Nir Lisker (@nlisker) have been working on.  It's a complete implementation including good test coverage.  
> 
> This was based on https://github.com/openjdk/jfx/pull/434 but with a smaller API footprint.  Compared to the PoC this is lacking public API for subscriptions, and does not include `orElseGet` or the `conditionOn` conditional mapping.
> 
> **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: https://github.com/openjdk/jfx/pull/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`.
> 
> 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
>       ));
> 
>       // Standard JavaFX + Optional:
>       label.textProperty().bind(Bindings.createStringBinding(
>           () -> Optinal.ofNullable(employee.get())
>               .map(Employee::getCompany)
>               .map(Company::getName)
>               .orElse(""),
>          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")
>       );
>     }
> 
> 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 Hendrikx has updated the pull request incrementally with two additional commits since the last revision:

 - Add null checks in Subscription
 - Update copyrights

-------------

Changes:
  - all: https://git.openjdk.org/jfx/pull/675/files
  - new: https://git.openjdk.org/jfx/pull/675/files/2c3a9d95..6ae74d19

Webrevs:
 - full: https://webrevs.openjdk.org/?repo=jfx&pr=675&range=16
 - incr: https://webrevs.openjdk.org/?repo=jfx&pr=675&range=15-16

  Stats: 14 lines in 10 files changed: 4 ins; 0 del; 10 mod
  Patch: https://git.openjdk.org/jfx/pull/675.diff
  Fetch: git fetch https://git.openjdk.org/jfx pull/675/head:pull/675

PR: https://git.openjdk.org/jfx/pull/675


More information about the openjfx-dev mailing list