An open API service indexing awesome lists of open source software.

https://github.com/hjohn/hs.jfx.eventstream

Light-weight Streams for JavaFX
https://github.com/hjohn/hs.jfx.eventstream

Last synced: about 1 year ago
JSON representation

Light-weight Streams for JavaFX

Awesome Lists containing this project

README

          

# Streams for JavaFX

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.hjohn.jfx.eventstream/eventstream-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.hjohn.jfx.eventstream/eventstream-core)
[![Build Status](https://github.com/hjohn/hs.jfx.eventstream/workflows/master/badge.svg)](https://github.com/hjohn/hs.jfx.eventstream/actions)
[![Coverage](https://codecov.io/gh/hjohn/hs.jfx.eventstream/branch/master/graph/badge.svg?token=QCNNRFYF98)](https://codecov.io/gh/hjohn/hs.jfx.eventstream)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Based on the work by Tomas Mikula's excellent ReactFX project (https://github.com/TomasMikula/ReactFX)
and used with permission.

## Overview

This library allows streaming values generated by properties or changes triggered by JavaFX
events with an expressive fluent API which also enables easier management of listeners to
prevent memory leaks.

### Basics

Streams can be used to listen to property changes and take an action each time the
property changes. A simple example below shows how to print the value of a button's text
property to the console each time it changes:

Changes.of(button.textProperty())
.subscribe(System.out::println);

A more sophisticated example may want to convert the text to upper case and replace a
`null` value with an empty string:

Changes.of(button.textProperty())
.map(String::toUpperCase)
.orElse("")
.subscribe(System.out::println);

To also print the initial value of the text property another print statement could be
included, but alternatively a value stream can be used. A property of a value stream is
that it will send the current value immediately to new subscribers:

Values.of(new SimpleStringProperty("Hello World"))
.subscribe(System.out::println); // prints the value immediately

### Null Handling

In the earlier examples `null` was not explicitly handled in the `map` function. This is
because these functions are null-safe. The functions are not called when the value emitted
is `null`. In order to handle `null`s, streams offer similar functions to Java's `Optional`
to deal with the case when `null` is emitted.

These functions make dealing with `null` a lot simpler as not every `map`, `flatMap` or
`filter` step specifically needs to deal with it. For example, when creating a stream with
several chained properties, the `null` checks can be omitted:

Values.of(button.sceneProperty())
.flatMap(scene -> Values.of(scene.windowProperty())) // scene won't be null :)
.flatMap(window -> Values.of(window.showingProperty()))
.orElse(false) // deals with the case when either scene or window is null
.subscribe(showing -> System.out.println("showing is: " + showing));

### Stream Types

This library offers three types of streams, each with distinct characteristics. Their
differences lie in how they treat `null` and what happens when they are subscribed. See
the summary in the table below:

|Type |Null Values |Upon Subscription |
|-------------|:-----------:|:-----------------------------:|
|Event Stream |Discarded |Sends nothing |
|Change Stream|Allowed |Sends nothing |
|Value Stream |Allowed |Sends current value |

#### Event Streams

Event streams are the most basic type of stream. When an event occurs, this stream emits a
value, which can be mapped and filtered as needed. Event streams never emit `null`, and
mapping an event to `null` will discard the event. Event streams only deliver events when
they occur, thus if after subscribing no events occur nothing will be delivered.

#### Change Streams

Change streams are event streams which allow null values and mapping to null values.
Its methods are null safe, and a separate set of methods is available to deal with null
cases, similar to Java's `Optional`. This means that methods like `filter`, `map` and
`flatMap` never have to deal with their input being `null`, instead use methods like
`orElse` and `orElseGet`. Subscribers can still receive `null`, unlike event streams.

#### Value Streams

Value streams expand further upon change streams with a notion of a current value which
new subscribers will immediately receive as their first value. This makes value streams very
suitable as a binding source for a property as they immediately emit their current state.

### Conversions between Stream Types

The different types of streams can be converted into one or the other with a few specific
methods. Usually these methods could violate an invariant of the current stream type but
allow this by returning a different stream type for which this is allowed.

For example, filtering a value stream may result in the stream not emitting a value upon
subscription making it unsuitable as a binding. The `filter` method therefore returns a
change stream instead. Another example is changing a change stream into a value stream
with `withDefault`. This method assigns a default value to the stream which can be emitted
immediately upon subscription.

The table below shows which of the most commonly used functions are available for each
stream type and whether the type of stream changes as a result:

| Function | Event | Change | Value |
| --------------------------- |:-----:|:------:|:-----:|
| map, flatMap | X | X | X |
| filter | X | X | X(C) |
| peek | X | X | X |
| withDefault, withDefaultGet | X(V) | X(V) | - |
| orElse, orElseGet | - | X | X |
| or | - | X | X |
| conditionOn | X | X | X |
| flatMapToChange | - | - | X(C) |

The following table shows which terminal operations are available for each stream type:

| Function | Event | Change | Value |
| --------------------------- |:-----:|:------:|:-----:|
| subscribe | X | X | X |
| toBinding | - | - | X |

### Lazy Subscriptions

Streams only observe their source when a consumer is currently subscribed.

ValueStream vs = Values.of(button.textProperty())
.map(String::toUpperCase);

In the above example, the button's text property is not observed until an actual subscriber
is added to the stream:

vs.subscribe(System.out::println);

Streams of this type are called lazy streams. All streams provided by this package are lazy
and will only observe their source when needed.

As lazy streams will stop observing their source when they have no subscribers, the source
stream will not prevent garbage collection when there are no more subscribers. There is
therefore no need to use weak listeners for observing the source stream.

An advantage of this approach is that an example like below will function as one would
expect, and will keep printing changes in `button.textProperty()`:

Values.of(button.textProperty())
.map(t -> t + "World")
.subscribe(System.out::println);

Contrast this with JavaFX's standard binding mechanism which may garbage collect the binding
at any time because of its use of weak listeners:

button.textProperty()
.concat("World") // weak binding used here
.addListener((obs, old, current) -> System.out.println(current));

This can be very surprising, especially when adding the `concat` function at a later stage,
because that simple change will result in a completely different runtime behavior.

## Motivation

This project was created in the hope to add additional functionality directly to JavaFX to address
a few of its rough edges. Mainly:

- Type-safe `Bindings#select` functionality, which allows to create bindings to nested properties. The current implementation
is not type safe and does not offer much flexibility to customize the binding.

The project purposely contains only a small well defined subset of code adapted from ReactFX with the goal
of making the project easier to evaluate for potential inclusion into JavaFX. Direct inclusion would
offer major advantages by adding default methods to the `ObservableValue` and `Binding` interfaces making
classes that implement these interfaces act more like `Optional`'s would:

Binding quotedTitleText = model.titleProperty()
.map(text -> "'" + text "'"); // new `map` method on `Binding`

### Type-safe binding to nested properties

In standard JavaFX, creating a binding to a nested property is a cumbersome affair. One has to keep
track of the listeners to unregister them when a parent property changes, and reregister the listener
on the new value. With multiple levels of nesting this can quickly become complicated and error prone.

An example from JavaFX itself is the implementation of the `treeShowing` property. It tracks whether
or not a `Node` is currently showing on the screen. In order to do this, it must check if the `Node`
has a `Scene`, whether the `Scene` has a `Window`, and whether that `Window` is currently shown:

ChangeListener windowShowingChangedListener = (win, oldVal, newVal) -> updateTreeShowing();

ChangeListener sceneWindowChangedListener = (scene, oldWindow, newWindow) -> {
if (oldWindow != null) {
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
}
if (newWindow != null) {
newWindow.showingProperty().addListener(windowShowingChangedListener);
}
updateTreeShowing();
};

ChangeListener sceneChangedListener = (node, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.windowProperty().removeListener(sceneWindowChangedListener);

Window window = oldScene.windowProperty().get();
if (window != null) {
window.showingProperty().removeListener(windowShowingChangedListener);
}
}
if (newScene != null) {
newScene.windowProperty().addListener(sceneWindowChangedListener);

Window window = newScene.windowProperty().get();
if (window != null) {
window.showingProperty().addListener(windowShowingChangedListener);
}
}

updateTreeShowing();
};

This can already be expressed much more succintly by using the `Bindings#select` function:

BooleanProperty treeShowing = Bindings.selectBoolean(node.sceneProperty(), "window", "showing");

The method however is not type safe. A mistake in one of the string parameters or the choice of select
method will lead to errors at runtime. It will also complain about `null` values and map them to some
standard value.

#### Alternative solution using Streams

With streams we can create the same binding in a type-safe manner:

Binding treeShowing = Values.of(node.sceneProperty())
.flatMap(s -> Values.of(s.windowProperty()))
.flatMap(w -> Values.of(w.showingProperty()))
.orElse(false)
.toBinding();

This is far less cumbersome and still 100% type safe.

### Preventing memory leaks

When you bind a property in JavaFX you have to carefully consider the lifecycle of the two properties
involved. Calling `bind` on a property will keep a target property synced with a source property.

target.bind(source); // keep target in sync with source

This is equivalent to adding a (weak) listener (weak listener code omitted here) and keeping
track of the property target was bound to:

source.addListener((obs, old, current) -> target.set(current));
target.getProperties().put("boundTo", source);

In both these cases:

- source refers to target through the listener added because it needs to update the target when it changes
- target refers to source as the property it is "bound to" in order for `unbind` to do its magic

In JavaFX, `bind` will use a weak listener, which means that the target can be garbage collected independently
from the source property. However, the reference from target to source with its "bound to" property is a hard
reference (if it were weak then a binding could stop working without notice because the source could be
garbage collected and stop sending its updates). This means that the lifecycle of the source property is
now closely tied to the target property.

If the target property is a long-lived object (like a data model) and the source property is a shorter lived
object like a UI element, you could have inadvertently created a big memory leak; all UI components
have a parent and a scene property, so effectively keeping a reference to any UI element can keep an entire
scene or window from being garbage collected.

Something as simple as keeping the selection of a `ListView` in sync with a model can lead to this:

model.selectedItemProperty()
.addListener((obs, old, current) -> listView.getSelectionModel().select(current));

Assuming `model` here is a long-lived object that perhaps is re-used next time the UI is shown to remember
the last selected item, the listener as shown above will prevent the `ListView` and all other UI components
that it refers to from being garbage collected.

To prevent this one must remember to wrap the above listener in a `WeakChangeListener` and be careful not
to keep a reference around to the unwrapped change listener. Using a weak listener is not a perfect
solution however. The listener only stops working when a garbage collection cycle runs and in the mean
time the UI code may interfere with normal operations if the selected item is changed in the model, as it
could trigger code in the (soon to be garbage collected) UI, which may still trigger other changes.

It might be better to disable the listener as soon as the UI is hidden. Doing this manually means keeping
track of the listeners involved that may need unregistering (and potentially reregistering if the UI becomes
visible again). Instead we could listen to a property that tracks the showing status of our UI.
Unfortunately, this is somewhat involved as there is no easy property one can listen to; you have to listen
for `Scene` changes, check which `Window` it is associated with and then bind to `Window::showingProperty`
-- and update these listeners if the scene or window changes.

With Streams one could safely bind a UI property to a model only when the UI is visible:

model.selectedItemProperty()
.conditionOn(isShowing)
.subscribe(selectedItem -> listView.getSelectionModel().select(selectedItem));

Where the `isShowing` variable can be created like this:

Binding isShowing = Values.of(listView.sceneProperty())
.flatMap(s -> Values.of(s.windowProperty()))
.flatMap(w -> Values.of(w.showingProperty()))
.orElse(false)
.toBinding();

Or with a small helper class, which only needs the `Node` involved as a parameter:

model.selectedItemProperty()
.conditionOn(Helper.isShowing(listView))
.subscribe(selectedItem -> listView.getSelectionModel().select(selectedItem));

The above binding to `model.selectedItemProperty()` will only be present while `listView` is
showing. If the list view is hidden, the listener is unregistered, and if it is shown again
the listener is re-added. If the UI is hidden, it will instantly stop reacting to any
changes in the model and (if also no longer referenced) will eventually be garbage collected.