par , 10/10/2024 à 07h47 (2318 Affichages)
Salut,
de passage à nouveau après quelques temps d'absence (même si toujours présent sur la modération du forum). Je me suis penché ces derniers temps sur les nouveautés apportées dans Java et JavaFX 21 à 23. Parmi les choses qui méritent d’être relevées, on peut trouver :
- Le fait de ne plus avoir besoin de nommer les variables inutilisées dans les lambda (Java 22)
- Le support des animations simples dans les CSS (JavaFX 23)
- Les abonnements sur les propriétés (JavaFX 21)
C’est sur ce dernier point que je vais revenir aujourd’hui car il se trouve être un complément à ma série d’articles tournant autour des propriétés en JavaFX :
Précédemment, pour connaitre les changements sur la valeur contenue dans une propriété, il fallait enregistrer des écouteurs de type InvalidationListerner ou ChangeListener :
- InvalidationListener - un type d’évènement léger qui prévient que la valeur contenue dans la propriété n’est plus valide (invalidée), et ne fournit plus d’autre notification tant que la valeur n’est pas revalidée (ce qui se passe au premier accès direct à la propriété) ;
myTextField.textProperty().addListener(observable -> { [
] })
- ChangeListener - un type d’évènement lourd (récurrent) qui prévient que la valeur dans la propriété a changé et fournit des références sur la propriété, l’ancienne valeur et la nouvelle valeur.
myTextField.textProperty().addListener((observable, oldValue, newValue) -> { [
] });
Abonnement
Les abonnements (subscriptions) se trouvent donc être un moyen de simplifier encore plus le code (et la gestion des écouteurs) lorsqu’on gère des propriétés observables. Chaque propriété se trouve désormais affublée d’une nouvelle méthode nommée subscribe() qui dispose de 3 surcharges :
Runnable (lambda sans paramètre)
Cette méthode est définie dans l'interface javafx.beans.Observable, là où est défini le contrat de gestion des InvalidationListener . Elle sert exactement à faire la même chose : être prévenu que la valeur de la propriété est désormais invalide. Le Runnable passé en argument sera exécuté une seule fois tant que la valeur n’est pas à nouveau revalidée ; donc des changements successifs de valeur passeront totalement inaperçus.
myTextField.textProperty().addListener(observable -> { [
] })
Devient :
myTextField.textProperty().subscribe(() -> { [
] })
Les 2 autres méthodes sont définies dans l'interface ObservableValue<T>, là où est défini le contrat de gestion des ChangeListener. De la même manière, ces 2 surcharges font exactement la même chose que des écouteurs de changement : être prévenu des variation de la valeur de la propriété.
Consumer (lambda avec 1 paramètre)
Le paramètre qui sera fourni au Consumer correspond à la nouvelle valeur de la propriété. De plus, ce consommateur sera invoqué immédiatement après la mise en place de l’abonnement.
En effet, les propriétés ont souvent déjà une valeur initiale quand on les manipule et, quand on monte une interface graphique, il est donc assez courant de devoir écrire :
1 2 3 4 5
| myTextField.textProperty().addListener((observable, oldValue, newValue) -> {
updateWithText (newValue);
});
// Mettre à jour avec la valeur déjà présente.
updateWithText(newValue); |
La présence de cette méthode permet donc de faire simplement :
1 2 3
| myTextField.textProperty().subscribe(newValue -> updateWithText(newValue));
// Ou
myTextField.textProperty().subscribe(this::updateWithText); |
BiConsumer (lambda avec 2 paramètres)
Les deux paramètres fournis au BiConsumer correspondent à l’ancienne et à la nouvelle valeur de la propriété.
myTextField.textProperty().addListener((observable, oldValue, newValue) -> { [
] });
La présence de cette méthode permet donc de faire simplement :
myTextField.textProperty().subscribe((oldValue, newValue) -> { [
] });
En revanche, ici le BiConsumer n’est pas invoqué immédiatement après l’abonnement puisque la valeur de la propriété n’a pas encore été modifiée.
Désabonnement
Un souci majeur de la gestion des évènements par écouteurs était qu'il fallait conserver des références sur ces derniers, et ce de manière à les désenregistrer et éviter des soucis tels que des propagations d'évènements non-souhaités même après que les composants ou dialogues aient été cachés ou encore pour pallier aux potentielles fuites mémoire. Il était donc assez courant devoir écrire :
1 2 3 4 5 6 7 8 9
| private final PropertyChangeListener<String> textChangeListener = (observable, oldValue, newValue) -> {};
// Lors de la construction de l'interface graphique.
myTextField.textProperty().addListener(textChangeListener);
[...]
// Lors de la destruction de l'interface graphique.
myTextField.textProperty().removeListener(textChangeListener); |
Et cela ne fonctionnait pas du tout d'ailleurs avec les références de méthode :
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void reactToTextChange(ObservableValue<String> observable, String oldValue, String newValue {
[
]
}
// Lors de la construction de l'interface graphique.
// Fonctionne OK.
myTextField.textProperty().addListener(this::reactToTextChange);
[...]
// Lors de la destruction de l'interface graphique.
// Pas OK car on a une autre référence différente de la 1ere.
myTextField.textProperty().removeListener(this::reactToTextChange); |
Ici c'est plus facile car chacune des méthodes subcribe() renvoie un objet de type javafx.util.Subscription sur lequel il suffit d'invoquer la méthode unsubscribe()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
private Subscription textSubscription;
// Lors de la construction de l'interface graphique.
textSubscription = myTextField.getProperty().subscribe(newValue -> { [
] });
// Ou
private void updateToNewText(final String newValue) {
[...]
};
textSubscription = myTextField.getProperty().subscribe(this::updateToNewText);
[...]
// Lors de la destruction de l'interface graphique.
textSubscription.unsubscribe()
// Ou
Optional.ofNullable(textSubscription)
.ifPresent(Subscription::unsubscribe); |
Conclusion
Nous en avons fini avec ce petit tour rapide des abonnements qui permettent de simplifier et d’unifier la gestion des invalidations et des changements de valeur des propriétés, ainsi que de gérer plus facilement les désabonnements.
Grosso modo, dans un contrôle customisé, il devrait être suffisant de faire comme suit :
1 2 3 4 5 6 7 8 9 10
| private List<Subscription> subscriptions = new LinkedList<>();
// Lors de la construction de l'interface graphique.
subscriptions.add(aSubControl.aProperty().subscribe(
));
[...]
// Lors de la destruction de l'interface graphique.
subscriptions.forEach(Subscription::unsubscribe);
subscriptions.clear(); |