-
Notifications
You must be signed in to change notification settings - Fork 47
EventStream Intro and Basic Usage
EventStreams come from the Reactive Programming Paradigm. This page should give enough explanation for one to understand how to use EventStream in their code without knowing too much about the paradigm. However, if one is unfamiliar with the paradigm and wants a more in-depth explanation, see Helpful Reactive Programming Resources.
JavaFX has a representation of a time-varying value, namely ObservableValue. ObservableValue holds a value at any point in time. This value can be requested with getValue().
Events, on the other hand, are ephemeral—they come and go. You can only be notified of an event when it occurs;—it does not make sense to ask the event stream about the "current event".
JavaFX has means to compose observable values to form new observable values, either using the fluent API (methods of ObservableValue subclasses), or using the Bindings helper class. Some useful compositions of observable values are also provided by the EasyBind library.
JavaFX, however, does not have a nice way to compose streams of events. The user is left with event handlers/listeners, which are not composable and inherently side-effectful. EventStreams try to fill this gap.
For example, using ChangeListeners
, EventHandlers
, and everything else the JavaFX API provides, how difficult would it be for a developer to write code that must run only when the following conditions are true:
- The last letters that were pressed by the user were "a", "b", and "c"
- The user hovered the mouse over a circle for three seconds
- After hovering, the user clicks the mouse.
- A
SimpleBooleanProperty
is true - A second
SimpleBooleanProperty
is false
Writing the above code with EventStream
objects is not only possible but easy.
Through examples, this section introduces the most basic things that one needs to know about EventStream
objects and how to use them properly.
Although there are various ways to create an EventStream
, the most simple one is using the EventStreams
class. Note the difference: EventStream
is the actual Stream object; EventStreams
is a class with factory methods used to create an EventStream
object. For example, mouse clicks:
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
// create an EventStream using EventStreams factory methods
EventStream<MouseEvent> sceneClicks = EventStreams.eventsOf(scene, MouseEvent.MOUSE_CLICKED);
// The class inside the angle brackets "<MouseEvent>" is the kind of event that the EventStream emits.
}
The above assignment should be read as "every time the user clicks on scene, sceneClicks
will emit that MouseEvent as an Event."
Once an EventStream is created, one can use it to create additional EventStream objects through a process called "composition." Returning to our previous example, knowing when a user clicks is useful. However, what if one only wants to know the x
and y
coordinates of that mouse click? This is where composition comes into play:
// Read: when sceneClicks emits a MouseEvent, clickCoordinates will emit
// a Point2D object that is constructed using the MouseEvent's coordinates.
EventStream<Point2D> clickCoordinates = sceneClicks.map(event -> new Point2D(event.getX(), event.getY());
Now that we've shown a basic example of how to create and compose an EventStream
, what does one do with it?
When an EventStream
emits a new Event, one needs to be notified. This is called 'subscribing' to the EventStream. Below is the most basic way to subscribe to an EventStream. Other ways will be covered later.
// Read: every time sceneClicks emits an event, print it
sceneClicks.subscribe(click -> System.out.println("Mouse was clicked: " + click.toString());
Rectangle rectangle = new Rectangle();
// Read: every time clickCoordinates emits an event, relocate rectangle's top-left corner
// to that click location.
clickCoordinates.subscribe(point -> rectangle.relocate(point.getX(), point.getY());
If you've looked at the API, you'll notice that the subscribe()
method returns a Subscription
object. What is that?
When using JavaFX ChangeListeners
or EventHandlers
, one would typically do something like this:
// create an EventHandler
EventHandler<MouseEvent> handler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
System.out.println("Mouse was clicked: " + event.toString()
}
}
// add Event Handler when needed
scene.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);
// remove Event Handler when its no longer needed.
// Forgetting this step will lead to a memory leak
scene.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
This isn't the best approach because, to remove an EventHandler, one needs a reference to the object itself (scene) and its EventHandler (handler). What happens if both references are gone or out of scope?
One might argue, "Well, why not use a WeakEventHandler
or some other implementation of WeakListener
?" There are actually problems using that approach as well. If you want to know more, read a blog post written by Tomas that examines this issue in more detail.
So, how does ReactFX deal with this issue? Returning to our example...
// all of the related information is stored in Subscription.
Subscription subscription = clicks.subscribe(click -> System.out.println("Mouse Event was: " + click.toString());
// Now, when one no longer wants to be notified of new events
// emitted by sceneClicks, one simply unsubscribes:
subscription.unsubscribe();
Forgetting to manage the returned Subscription
object will certainly create a memory leak. So, every time something subscribes to an EventStream, its returned Subscription
object needs to be stored for later unsubscribing. Now, one might think naïvely that they should do the following:
class SomeClass {
Subscription sub1
Subscription sub2
Subscription sub3
Subscription sub4
Subscription sub5
SomeClass(EventStream<?> one, EventStream<?> two,
EventStream<?> three, EventStream<?> four, EventStream<?> five) {
sub1 = one.subscribe(/* stuff */);
sub2 = two.subscribe(/* stuff */);
sub3 = three.subscribe(/* stuff */);
sub4 = four.subscribe(/* stuff */);
sub5 = five.subscribe(/* stuff */);
}
void dispose() {
sub1.unsubscribe();
sub2.unsubscribe();
sub3.unsubscribe();
sub4.unsubscribe();
sub5.unsubscribe();
}
}
What a headache! Fortunately, there's a better way!
Only one Subscription object needs to be used. There are various ways to do this:
- use the Subscription's
and(Subscription other)
method. (Useful when subscribing to an EventStream objects at various parts in a class).
class SomeClass {
Subscription subscription = () -> {};
SomeClass(EventStream<?> one, EventStream<?> two,
EventStream<?> three, EventStream<?> four, EventStream<?> five) {
manageSubscription(one.subscribe(/* stuff */));
manageSubscription(two.subscribe(/* stuff */));
manageSubscription(three.subscribe(/* stuff */));
manageSubscription(four.subscribe(/* stuff */));
manageSubscription(five.subscribe(/* stuff */));
);
}
void manageSubscription(Subscription other) {
subscription = subscription.and(other)
}
void dispose() {
subscription.unsubscribe();
}
}
- Use
Subscription.multi()
(Useful when subscribing to EventStreams that are all within the same area of code).
class SomeClass {
Subscription subscription
SomeClass(EventStream<?> one, EventStream<?> two,
EventStream<?> three, EventStream<?> four, EventStream<?> five) {
subscription = Subscription.multi(
one.subscribe(/* stuff */),
two.subscribe(/* stuff */),
three.subscribe(/* stuff */),
four.subscribe(/* stuff */),
five.subscribe(/* stuff */),
);
}
void dispose() {
subscription.unsubscribe();
}
}
As one creates and composes EventStream
objects, the "route" Event(s) take may get longer and more complex. ("Route" here describes the path Event(s) take, starting with the initial Event(s) emitted, continuing with the changes, modifications, or manipulations said Event(s) undergo, and ending with their emission from the final EventStream
object to that stream's subscribers.)
To debug an EventStream, use the hook()
method:
EventStream<KeyEvent> keyPressed = EventStreams.eventsOf(scene, KeyEvent.KEY_PRESSED);
Subscription sub = keypressed
.hook(e -> System.out.println("Key Pressed Event: " + e.toString());
.subscribe(e -> moveRectangleToPoint(e.getX(), e.getY());
Subscription mappedSub = keyPressed
.map(e -> e.getText())
.hook(key -> System.out.println("User pressed: " + key))
.filter(key -> key.isDigitKey())
.feedTo(phoneNumberTextField.textProperty());