-
Notifications
You must be signed in to change notification settings - Fork 542
PoC: Fluent bindings for ObservableValue #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PoC: Fluent bindings for ObservableValue #434
Conversation
|
👋 Welcome back jhendrikx! A progress list of the required criteria for merging this PR into |
e204c63 to
f45f3de
Compare
f45f3de to
5182697
Compare
|
I like this! About the memory leaks; in Angular it is common that (the equivalent of) an addListener returns a subscription object, which can be closed when the enclosing service or component closes. This will break the strong reference and thus solve the memory leak. Is it an idea to do something similar here? AddListener is void, so it should be possible to have it return a Subscription object without breaking the API. The subscription object knows the exact listener, so it only removes that one. Of course that solution has the drawback in that you need to maintain an administration so you know what to close. Either a bunch of instance variables or a list, in Angular there is some other trickery but that is technology specific. It all comes down to that you can mark something to need closing at the point of subscribing. The best would be to bind that to an onClose event of the containing object, maybe a little helper method would be good, something like. this.cascadeClose( listView.getSelectionModel().selectedItemProperty().addListener( Or: |
|
There were `subscribe` methods added which return a `Subscription`. I
didn't change the addListener methods as I want to keep the changes to
existing code minimal to make this easier to review and perhaps get
accepted.
The returned subscriptions can be "and"-ed together to make a single
subscription which cancels all of them at once. I didn't extend this yet
with more API, but ReactFX has many good ideas in that area.
For memory leaks however, I really think the `conditionOn` feature that
was also in ReactFX is one of the best ways. You can create bindings and
subscriptions that way which automatically stop observing when parts of
the UI are removed or closed, which then can be garbage collected. This
is basically how I do all my memory management now, and rarely do I need
to actually remove a listener explicitely.
Your `closeWith` idea is similar to `conditionOn`, since it will
automatically unsubscribe when a condition is false. You could tie it to
the visible property for example, but also to a single boolean property
that is set to false when you need all the listeners to stop. A more
convenient method could be added (like `conditionOnShowing` which
directly accepts a `Node`), or `Node` could get a `showingProperty()`.
…On 25/03/2021 07:42, Tom Eugelink wrote:
I like this!
About the memory leaks; in Angular it is common that (the equivalent of)
an addListener returns a subscription object, which can be closed when
the enclosing service or component closes. This will break the strong
reference and thus solve the memory leak. Is it an idea to do something
similar here? AddListener is void, so it should be possible to have it
return a Subscription object without breaking the API. The subscription
object knows the exact listener, so it only removes that one.
Of course that solution has the drawback in that you need to maintain an
administration so you know what to close. Either a bunch of instance
variables or a list, in Angular there is some other trickery but that is
technology specific. It all comes down to that you can mark something to
need closing at the point of subscribing. The best would be to bind that
to an onClose event of the containing object, maybe a little helper
method would be good, something like.
this.*cascadeClose*(
listView.getSelectionModel().selectedItemProperty().addListener(
(obs, old, current) -> longLivedModel.lastSelectedProperty().set(current)
));
Or:
listView.getSelectionModel().selectedItemProperty().addListener(
(obs, old, current) -> longLivedModel.lastSelectedProperty().set(current)
).*closeWith*(this);
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#434 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAHTETMXKRBWNTXSQZLFAV3TFLLL5ANCNFSM4ZP6W6JA>.
|
|
Changing addListener from void to something different would be binary incompatible change! |
I know, but adding a new method seems like an even worse idea, polluting the API more. |
|
I really like this idea. But even after a few days of looking at it, I still struggle with the "flatMap" operator. While I understand why you named it like that, it looks a bit foreign to me when it's used to select nested properties. Have you considered using "select" instead, which already has precedence in JavaFX? Is it possible to have something like an "orDefault" operator, which would be equivalent to "orElse" but with an implicit default value. |
The name is basically taken from ReactFX, Optional and Stream and is basically short-hand syntax for
Perhaps yes, but as the PoC doesn't have specific implementations for primitive types it may be a bit tricky to determine what the default should be as I think there is no type information to base this on. I assume you'd want it to be |
|
To me orDefault doesn't give me warm feelings, because it is a bit magical. I'd rather type orElse(0), then know for sure what its value is. I personally also never rely on defaults for any variable, even primitives, I always initialize them. |
That seems like a particular personal preference. Having sensible defaults is a large part of what makes Java as great a language as it is. |
|
Given my preference on initializing everything, it is obvious I have a different opinion on that matter :-)
And given my preference on initializing everything, it is obvious I have a different opinion :-) |
|
I really like this PR and the concepts it proposes. In fact, I'm maintaining a small library (originally due to @TomasMikula) that implements many of these features: https://github.com/tobiasdiez/EasyBind. Please use it as a source of inspiration and code as you see fit. There are a few notable differences in EasyBind:
Maybe it's a good idea to split this PR into smaller ones that only introduce a particular concept (i.e. separate mapper, listener, selection etc). |
|
On 20/08/2021 10:14, Tobias Diez wrote:
I really like this PR and the concepts it proposes. In fact, I'm
maintaining a small library (originally due to @TomasMikula
<https://github.com/TomasMikula>) that implements many of these
features: https://github.com/tobiasdiez/EasyBind. Please use it as a
source of inspiration and code as you see fit.
There are a few notable differences in EasyBind:
* Handling of null values: Sometimes you do want to have explicit
control of null values in your mapper. Thus, with EasyBind, we
simple apply the mapper to the current value, even if that's null.
On the other hand, we introduced |ObservableOptionalValue| as a
convenient wrapper around |ObservableValue<Optional<T>>| to handle
null values similar to |Optional|.
Yes, this is probably one of the more controversial choices in the proof
of concept. In this case the functionality of Optional was directly
integrated to make it easier to do map/flatMap chains without having to
deal with null.
The way EasyBind handles this can certainly be considered as well.
I also considered providing a different `map` function that does allow
`null`, but felt that it would be hard to think of a good name for it
(`mapNullable`?) and it would just offer a second option for something
that was already possible.
* For mapping, there are two use cases: Either you have a simple
mapper as in |text.map(String:toUppercase)|, or sometimes the mapper
returns an observable value itself, which needs to be unpackaged.
I'm not sure I understand, but I think that's what flatMap does in the
PoC. It takes a function which returns an ObservableValue. Are you
suggesting 'map' should unpackage the value if it happens to be an
instance of ObservableValue?
* For selection / flatmap chain, you often want to get a bona-fide
property back for which you can also set values (and not only use it
as the source for a binding).
Do you have an example case for where this is convenient? I generally
only used the most basic functionality of ReactFX, and haven't
encountered a scenario where I'd need this.
How does this deal with chains that don't result in a property (because
a property in the chain is null)?
Maybe it's a good idea to split this PR into smaller ones that only
introduce a particular concept (i.e. separate mapper, listener,
selection etc).
Yes, I tried to keep it very small (it is only 400 lines) but it can be
split further, or at least, not all API needs to be public right away as
Nir Lisker also suggested.
I've actually been using this PoC for a while now using a simple Maven
trick to extend ObservableValue and ObjectBinding without actually
having to build JavaFX (the Maven project depends on openjfx, but
overlaps the packages with those classes and creates an artifact with
those classes replaced). Depending on this new Maven artifact just works
with full code completion for the new methods.
I'm willing to put all the necessary work in to make this a success, but
would need some guidance as to what is needed to push this forward, if
it is currently considered at all.
…--John
|
|
On 20/08/2021 10:14, Tobias Diez wrote:
> * Handling of null values: Sometimes you do want to have explicit
> control of null values in your mapper. Thus, with EasyBind, we
> simple apply the mapper to the current value, even if that's null.
> On the other hand, we introduced |ObservableOptionalValue| as a
> convenient wrapper around |ObservableValue<Optional<T>>| to handle
> null values similar to |Optional|.
Yes, this is probably one of the more controversial choices in the proof
of concept. In this case the functionality of Optional was directly
integrated to make it easier to do map/flatMap chains without having to
deal with null.
The way EasyBind handles this can certainly be considered as well.
I also considered providing a different `map` function that does allow
`null`, but felt that it would be hard to think of a good name for it
(`mapNullable`?) and it would just offer a second option for something
that was already possible.
Just brain storming here, but would it be possible to make handling null an option on a stream that can be set to true?
.allowNull(true)
After which map and all allow null without an api change? Maybe by returning a different implementation when calling the map() etc methods.
Possibly even temporarily;
.allowNull(true)
.map(canBeNull -> ...)
.allowNull(false)
.map(cannotBeNull -> ...)
|
modules/javafx.base/src/main/java/javafx/beans/value/LazyObjectBinding.java
Show resolved
Hide resolved
|
@hjohn I think that this can be closed now :) |
|
Right :) |
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 withflatMap.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
Nodewithout listeners being created when the binding itself is not used (this would allow for the inclusion of atreeShowingPropertyinNodewithout creating excessive listeners, see this fix I did in an earlier PR: #185)Null Safe
The
mapandflatMapmethods are skipped, similar tojava.util.Optionalwhen the value they would be mapping isnull. This makes mapping nested properties withflatMaptrivial as thenullcase 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 withorElseororElseGet.Conditional Bindings
Bindings can be made conditional using the
conditionOnmethod. A conditional binding retains its last value when its condition isfalse. Conditional bindings donot observe their source when the condition isfalse, 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:
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.
I've created tests for this as well, but they're written in JUnit 5, which I noticed is not yet part of OpenJFX. A screenshot:
Progress
Download
To checkout this PR locally:
$ git fetch https://git.openjdk.java.net/jfx pull/434/head:pull/434$ git checkout pull/434To update a local copy of the PR:
$ git checkout pull/434$ git pull https://git.openjdk.java.net/jfx pull/434/head