Skip to content

EventStream Intro and Basic Usage

Denis Borisevich edited this page Mar 16, 2018 · 6 revisions

Introduction

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.

What is an EventStream and why use it?

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.

The Basics

Through examples, this section introduces the most basic things that one needs to know about EventStream objects and how to use them properly.

Creating an EventStream

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."

EventStream Composition

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());

EventStreams and Memory Leaks

Now that we've shown a basic example of how to create and compose an EventStream, what does one do with it?

Subscribing to an EventStream

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());

Preventing Memory Leaks

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();

Managing Subscriptions

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();
    }
}

Debugging EventStreams using hook()

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());