## Current Situation
`OberservableValue` offers `addListener(ChangeListener)` and `addListener(InvalidationListener)` for listening to changes or invalidations. When using these methods, great care must be taken to call their `removeListener` counterpart with the exact same value as was used before. Using an incorrect value will not result in an exception or warning, but is silently ignored.
With the introduction of Lambda's and Method references, it is much more common to write listeners in a very compact form:
property.addListener((obs, old, current) -> { ... });
property.addListener(this::method);
Unregistering listeners registered in the above way is not possible as the reference used cannot be recreated (Java does not guarantee they are the same reference if created again), hence this code is incorrect:
property.removeListener((obs, old, current) -> { ... });
property.removeListener(this::method);
This is made worse when unregistration of listeners that didn't exist does not result in an exception or warning.
Therefore, when wanting to unregister these listeners, the created listeners must be stored in a variable:
ChangeListener<String> cl = (obs, old, current) -> { ... };
InvalidationListener il = this::method;
All in all, this makes listener management quite verbose, as well as error prone when removing them.
## The Subscription class
Listener management can be made less error prone by not requiring the original listener reference in order to unregister it. Instead, registering a listener results in a `Subscription` which can be used to undo the registration. As the `Subscription` embeds a reference to the original listener, it is always the correct one, making use of Lambda's and Method references easier and safer:
// Add an invalidation listener:
Subscription s = property.invalidations(this::method);
// Unsubscribe the listener:
s.unsubscribe();
The subscription here represents the action or actions that must be taken to undo a previous action or group of actions. Multple actions can be aggregated into a single subscription, allowing all these actions to be triggered with a single call to `unsubscribe`.
A `Subscription` class already exists as part of the private API, and this proposal would make it public. In its simplest form its API looks like this:
@FunctionalInterface
interface Subscription {
void unsubcribe();
}
The `unsubscribe` method should be idempotent, and subsequent calls should not result in any actions taken.
Methods wishing to return a `Subscription` to undo a registration can often simply return a Lambda of the form:
return () -> property.removeListener(listener);
If the registration involves other resources or tracking, the returned `Subscription` can clean those up as well as long as care is taken that these are idempotent.
To allow aggregation of multiple subscriptions, two additional methods are provided and implemented by the interface:
/**
* Returns a subscription which combines all given subscriptions.
*/
static Subscription of(Subscription... subscriptions) { ... }
/**
* Returns a new subscription which combines this subscription
* and another subscription.
*/
default Subscription and(Subscription other) { ... }
Usage examples:
Subscription s1 = subscriptionSource.subscribe( ... );
Subscription s2 = subscriptionSource2.subscribe( ... );
Subscription all = s1.and(s2);
Subcription s = Subscription.of(
subscriptionSource.subscribe( ... ),
subscriptionSource2.subscribe( ... )
);
## Subscription sources
Armed with a `Subscription` class which makes unregistering listeners relatively fool proof and managing multiple listeners a lot simpler, we also need ways to obtain these subscriptions.
The most obvious sources are the current `addListener` methods on `ObservableValue`. These methods cannot be changed to return a `Subscription` as this is not a binary compatible change. However, default methods can be provided that leverage the existing API.
default Subscription invalidations(Runnable listener) {
addListener(obs -> listener.run());
return () -> removeListener(listener);
}
The above code registers an invalidation listener using the existing `addListener` API, and returns a Lambda which implements `Subscription` that calls `removeListener` with the exact same listener that was provided.
Proposed are the following three new methods to register listeners and obtain a subscription:
/**
* Subscribes to the changes of this observable, supplying
* both the old and the new value to the provided listener.
*/
default Subscription changes(BiConsumer<T, T> listener) { ... }
/**
* Subscribes to the values of this observable. The listener
* is called immediately with the current value.
*/
default Subscription values(Consumer<T> listener) { ... }
/**
* Subscribes to the invalidations of this observable.
*/
default Subscription invalidations(Runnable listener) { ... }
Note that these listeners omit the `Observable` parameter that would allow to distinguish which observable triggered the notification. More on this below.
Registering multiple listeners and unregistering would look like this:
Subscription s = Subscription.of(
label.fontProperty().invalidations(this::requestLayout),
label.textProperty().values(System.out::println),
label.wrapTextProperty().invalidations(this::requestLayout)
);
To dispose of these listeners, just call:
s.unsubcribe();
### The `values` listener and missing `Observable` parameter
When looking at how listeners are currently used, and change listeners in particular, we note that the extra `Observable` parameter is used in only extremely rare cases. An analysis of javafx.base and javafx.controls shows that change listeners are used in four different ways:
1. As a "heavy" invalidation listener, no parameters are used; in these cases the invalidation model may be a poor fit for the intended use (requiring immediate revalidation), or the listener could have been an invalidation listener with some tweaking.
2. As a value listener, the other parameters are unused; sometimes this is done indirectly and the value is obtained (again) by calling the property get method.
3. To manage listeners or event handlers, using the old value to unregister a listener and the new value to register a new one. A more concise alternative exists now via ObservableValue#flatMap for this use case.
4. To do something more complicated with both old and new value
Only a few instances were found that use the observable parameter, usually for highly specialized cases where potentially many items with the same functionality exist, like menu items or table columns.
Counting the different uses of change listeners resulted in 7, 18, 9 and 1 cases respectively. In other words, in 27 cases (>70%), a value listener could have been used. In most other cases, the less error prone `flatMap` could be used.
### Advantages of the simpler values listener
1. The Lambda and Method reference signatures for a value listener is much simpler, allowing more concise lambda's and easier re-use of existing methods that work with just the value (like `list::add` or `property::setValue`). Method signatures when using method references would not need to include up to two unused parameters: `method(Observable notNeeded, T notNeeded2, T value);`
2. The listener can be called immediately with the current value, avoiding an often seen pattern where a change listener is added, and then called one time manually to "sync" it:
Before:
property.addListener((obs, old, current) -> { setX(current); });
setX(property.getValue()); // initial value
After:
property.values(this::setX);
## Implementation
The above proposal could be implemented in several steps. First, `Subscription` is promoted to public API (excluding its current two `static` methods `subscribe` and `subscribeInvalidations`).
In a next step `ObservableValue` can be extended with new default methods for invalidations, changes and values.
`OberservableValue` offers `addListener(ChangeListener)` and `addListener(InvalidationListener)` for listening to changes or invalidations. When using these methods, great care must be taken to call their `removeListener` counterpart with the exact same value as was used before. Using an incorrect value will not result in an exception or warning, but is silently ignored.
With the introduction of Lambda's and Method references, it is much more common to write listeners in a very compact form:
property.addListener((obs, old, current) -> { ... });
property.addListener(this::method);
Unregistering listeners registered in the above way is not possible as the reference used cannot be recreated (Java does not guarantee they are the same reference if created again), hence this code is incorrect:
property.removeListener((obs, old, current) -> { ... });
property.removeListener(this::method);
This is made worse when unregistration of listeners that didn't exist does not result in an exception or warning.
Therefore, when wanting to unregister these listeners, the created listeners must be stored in a variable:
ChangeListener<String> cl = (obs, old, current) -> { ... };
InvalidationListener il = this::method;
All in all, this makes listener management quite verbose, as well as error prone when removing them.
## The Subscription class
Listener management can be made less error prone by not requiring the original listener reference in order to unregister it. Instead, registering a listener results in a `Subscription` which can be used to undo the registration. As the `Subscription` embeds a reference to the original listener, it is always the correct one, making use of Lambda's and Method references easier and safer:
// Add an invalidation listener:
Subscription s = property.invalidations(this::method);
// Unsubscribe the listener:
s.unsubscribe();
The subscription here represents the action or actions that must be taken to undo a previous action or group of actions. Multple actions can be aggregated into a single subscription, allowing all these actions to be triggered with a single call to `unsubscribe`.
A `Subscription` class already exists as part of the private API, and this proposal would make it public. In its simplest form its API looks like this:
@FunctionalInterface
interface Subscription {
void unsubcribe();
}
The `unsubscribe` method should be idempotent, and subsequent calls should not result in any actions taken.
Methods wishing to return a `Subscription` to undo a registration can often simply return a Lambda of the form:
return () -> property.removeListener(listener);
If the registration involves other resources or tracking, the returned `Subscription` can clean those up as well as long as care is taken that these are idempotent.
To allow aggregation of multiple subscriptions, two additional methods are provided and implemented by the interface:
/**
* Returns a subscription which combines all given subscriptions.
*/
static Subscription of(Subscription... subscriptions) { ... }
/**
* Returns a new subscription which combines this subscription
* and another subscription.
*/
default Subscription and(Subscription other) { ... }
Usage examples:
Subscription s1 = subscriptionSource.subscribe( ... );
Subscription s2 = subscriptionSource2.subscribe( ... );
Subscription all = s1.and(s2);
Subcription s = Subscription.of(
subscriptionSource.subscribe( ... ),
subscriptionSource2.subscribe( ... )
);
## Subscription sources
Armed with a `Subscription` class which makes unregistering listeners relatively fool proof and managing multiple listeners a lot simpler, we also need ways to obtain these subscriptions.
The most obvious sources are the current `addListener` methods on `ObservableValue`. These methods cannot be changed to return a `Subscription` as this is not a binary compatible change. However, default methods can be provided that leverage the existing API.
default Subscription invalidations(Runnable listener) {
addListener(obs -> listener.run());
return () -> removeListener(listener);
}
The above code registers an invalidation listener using the existing `addListener` API, and returns a Lambda which implements `Subscription` that calls `removeListener` with the exact same listener that was provided.
Proposed are the following three new methods to register listeners and obtain a subscription:
/**
* Subscribes to the changes of this observable, supplying
* both the old and the new value to the provided listener.
*/
default Subscription changes(BiConsumer<T, T> listener) { ... }
/**
* Subscribes to the values of this observable. The listener
* is called immediately with the current value.
*/
default Subscription values(Consumer<T> listener) { ... }
/**
* Subscribes to the invalidations of this observable.
*/
default Subscription invalidations(Runnable listener) { ... }
Note that these listeners omit the `Observable` parameter that would allow to distinguish which observable triggered the notification. More on this below.
Registering multiple listeners and unregistering would look like this:
Subscription s = Subscription.of(
label.fontProperty().invalidations(this::requestLayout),
label.textProperty().values(System.out::println),
label.wrapTextProperty().invalidations(this::requestLayout)
);
To dispose of these listeners, just call:
s.unsubcribe();
### The `values` listener and missing `Observable` parameter
When looking at how listeners are currently used, and change listeners in particular, we note that the extra `Observable` parameter is used in only extremely rare cases. An analysis of javafx.base and javafx.controls shows that change listeners are used in four different ways:
1. As a "heavy" invalidation listener, no parameters are used; in these cases the invalidation model may be a poor fit for the intended use (requiring immediate revalidation), or the listener could have been an invalidation listener with some tweaking.
2. As a value listener, the other parameters are unused; sometimes this is done indirectly and the value is obtained (again) by calling the property get method.
3. To manage listeners or event handlers, using the old value to unregister a listener and the new value to register a new one. A more concise alternative exists now via ObservableValue#flatMap for this use case.
4. To do something more complicated with both old and new value
Only a few instances were found that use the observable parameter, usually for highly specialized cases where potentially many items with the same functionality exist, like menu items or table columns.
Counting the different uses of change listeners resulted in 7, 18, 9 and 1 cases respectively. In other words, in 27 cases (>70%), a value listener could have been used. In most other cases, the less error prone `flatMap` could be used.
### Advantages of the simpler values listener
1. The Lambda and Method reference signatures for a value listener is much simpler, allowing more concise lambda's and easier re-use of existing methods that work with just the value (like `list::add` or `property::setValue`). Method signatures when using method references would not need to include up to two unused parameters: `method(Observable notNeeded, T notNeeded2, T value);`
2. The listener can be called immediately with the current value, avoiding an often seen pattern where a change listener is added, and then called one time manually to "sync" it:
Before:
property.addListener((obs, old, current) -> { setX(current); });
setX(property.getValue()); // initial value
After:
property.values(this::setX);
## Implementation
The above proposal could be implemented in several steps. First, `Subscription` is promoted to public API (excluding its current two `static` methods `subscribe` and `subscribeInvalidations`).
In a next step `ObservableValue` can be extended with new default methods for invalidations, changes and values.
- csr for
-
JDK-8311123 Subscription based listeners
- Closed