RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue
John Hendrikx
jhendrikx at openjdk.java.net
Thu Nov 18 21:04:15 UTC 2021
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
));
// 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.
-------------
Commit messages:
- Initial proposal
Changes: https://git.openjdk.java.net/jfx/pull/675/files
Webrev: https://webrevs.openjdk.java.net/?repo=jfx&pr=675&range=00
Issue: https://bugs.openjdk.java.net/browse/JDK-8274771
Stats: 1501 lines in 11 files changed: 1498 ins; 0 del; 3 mod
Patch: https://git.openjdk.java.net/jfx/pull/675.diff
Fetch: git fetch https://git.openjdk.java.net/jfx pull/675/head:pull/675
PR: https://git.openjdk.java.net/jfx/pull/675
More information about the openjfx-dev
mailing list