{"id":17257576,"url":"https://github.com/hjohn/hs.jfx.eventstream","last_synced_at":"2025-04-14T06:11:26.553Z","repository":{"id":53044908,"uuid":"309178864","full_name":"hjohn/hs.jfx.eventstream","owner":"hjohn","description":"Light-weight Streams for JavaFX","archived":false,"fork":false,"pushed_at":"2021-04-14T21:34:22.000Z","size":301,"stargazers_count":8,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-27T19:54:31.819Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hjohn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-11-01T20:09:21.000Z","updated_at":"2023-02-18T10:55:55.000Z","dependencies_parsed_at":"2022-09-05T07:10:18.695Z","dependency_job_id":null,"html_url":"https://github.com/hjohn/hs.jfx.eventstream","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2Fhs.jfx.eventstream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2Fhs.jfx.eventstream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2Fhs.jfx.eventstream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hjohn%2Fhs.jfx.eventstream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hjohn","download_url":"https://codeload.github.com/hjohn/hs.jfx.eventstream/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248830395,"owners_count":21168272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T07:17:56.266Z","updated_at":"2025-04-14T06:11:26.523Z","avatar_url":"https://github.com/hjohn.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Streams for JavaFX\n\n[![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)\n[![Build Status](https://github.com/hjohn/hs.jfx.eventstream/workflows/master/badge.svg)](https://github.com/hjohn/hs.jfx.eventstream/actions)\n[![Coverage](https://codecov.io/gh/hjohn/hs.jfx.eventstream/branch/master/graph/badge.svg?token=QCNNRFYF98)](https://codecov.io/gh/hjohn/hs.jfx.eventstream)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nBased on the work by Tomas Mikula's excellent ReactFX project (https://github.com/TomasMikula/ReactFX)\nand used with permission.\n\n## Overview\n\nThis library allows streaming values generated by properties or changes triggered by JavaFX\nevents with an expressive fluent API which also enables easier management of listeners to\nprevent memory leaks.\n\n### Basics\n\nStreams can be used to listen to property changes and take an action each time the\nproperty changes. A simple example below shows how to print the value of a button's text\nproperty to the console each time it changes:\n\n    Changes.of(button.textProperty())\n        .subscribe(System.out::println);\n\nA more sophisticated example may want to convert the text to upper case and replace a\n`null` value with an empty string:\n\n    Changes.of(button.textProperty())\n        .map(String::toUpperCase)\n        .orElse(\"\")\n        .subscribe(System.out::println);\n\nTo also print the initial value of the text property another print statement could be\nincluded, but alternatively a value stream can be used. A property of a value stream is\nthat it will send the current value immediately to new subscribers:\n\n    Values.of(new SimpleStringProperty(\"Hello World\"))\n        .subscribe(System.out::println);  // prints the value immediately\n\n### Null Handling\n\nIn the earlier examples `null` was not explicitly handled in the `map` function. This is\nbecause these functions are null-safe. The functions are not called when the value emitted\nis `null`. In order to handle `null`s, streams offer similar functions to Java's `Optional`\nto deal with the case when `null` is emitted.\n\nThese functions make dealing with `null` a lot simpler as not every `map`, `flatMap` or\n`filter` step specifically needs to deal with it. For example, when creating a stream with\nseveral chained properties, the `null` checks can be omitted:\n\n    Values.of(button.sceneProperty())\n        .flatMap(scene -\u003e Values.of(scene.windowProperty()))     // scene won't be null :)\n        .flatMap(window -\u003e Values.of(window.showingProperty()))\n        .orElse(false)  // deals with the case when either scene or window is null\n        .subscribe(showing -\u003e System.out.println(\"showing is: \" + showing));\n\n### Stream Types\n\nThis library offers three types of streams, each with distinct characteristics. Their\ndifferences lie in how they treat `null` and what happens when they are subscribed. See \nthe summary in the table below:\n\n|Type         |Null Values  |Upon Subscription              |\n|-------------|:-----------:|:-----------------------------:|\n|Event Stream |Discarded    |Sends nothing                  |\n|Change Stream|Allowed      |Sends nothing                  |\n|Value Stream |Allowed      |Sends current value            |\n\n#### Event Streams\n\nEvent streams are the most basic type of stream. When an event occurs, this stream emits a\nvalue, which can be mapped and filtered as needed. Event streams never emit `null`, and\nmapping an event to `null` will discard the event. Event streams only deliver events when\nthey occur, thus if after subscribing no events occur nothing will be delivered.\n\n#### Change Streams\n\nChange streams are event streams which allow null values and mapping to null values.\nIts methods are null safe, and a separate set of methods is available to deal with null\ncases, similar to Java's `Optional`. This means that methods like `filter`, `map` and\n`flatMap` never have to deal with their input being `null`, instead use methods like\n`orElse` and `orElseGet`. Subscribers can still receive `null`, unlike event streams.\n\n#### Value Streams\n\nValue streams expand further upon change streams with a notion of a current value which\nnew subscribers will immediately receive as their first value. This makes value streams very\nsuitable as a binding source for a property as they immediately emit their current state.\n\n### Conversions between Stream Types\n\nThe different types of streams can be converted into one or the other with a few specific\nmethods. Usually these methods could violate an invariant of the current stream type but\nallow this by returning a different stream type for which this is allowed.\n\nFor example, filtering a value stream may result in the stream not emitting a value upon\nsubscription making it unsuitable as a binding. The `filter` method therefore returns a \nchange stream instead. Another example is changing a change stream into a value stream\nwith `withDefault`. This method assigns a default value to the stream which can be emitted\nimmediately upon subscription.\n\nThe table below shows which of the most commonly used functions are available for each\nstream type and whether the type of stream changes as a result:\n\n| Function                    | Event | Change | Value |\n| --------------------------- |:-----:|:------:|:-----:|\n| map, flatMap                |   X   |    X   |   X   |\n| filter                      |   X   |    X   |  X(C) |\n| peek                        |   X   |    X   |   X   |\n| withDefault, withDefaultGet |  X(V) |   X(V) |   -   |\n| orElse, orElseGet           |   -   |    X   |   X   |\n| or                          |   -   |    X   |   X   |\n| conditionOn                 |   X   |    X   |   X   |\n| flatMapToChange             |   -   |    -   |  X(C) |\n\nThe following table shows which terminal operations are available for each stream type:\n\n| Function                    | Event | Change | Value |\n| --------------------------- |:-----:|:------:|:-----:|\n| subscribe                   |   X   |    X   |   X   |\n| toBinding                   |   -   |    -   |   X   |\n\n### Lazy Subscriptions\n\nStreams only observe their source when a consumer is currently subscribed.\n\n    ValueStream\u003cString\u003e vs = Values.of(button.textProperty())\n        .map(String::toUpperCase);\n\nIn the above example, the button's text property is not observed until an actual subscriber\nis added to the stream:\n\n    vs.subscribe(System.out::println);\n\nStreams of this type are called lazy streams. All streams provided by this package are lazy\nand will only observe their source when needed. \n\nAs lazy streams will stop observing their source when they have no subscribers, the source\nstream will not prevent garbage collection when there are no more subscribers. There is\ntherefore no need to use weak listeners for observing the source stream.\n\nAn advantage of this approach is that an example like below will function as one would\nexpect, and will keep printing changes in `button.textProperty()`:\n\n    Values.of(button.textProperty())\n        .map(t -\u003e t + \"World\")\n        .subscribe(System.out::println);\n\nContrast this with JavaFX's standard binding mechanism which may garbage collect the binding\nat any time because of its use of weak listeners:\n\n    button.textProperty()\n        .concat(\"World\")  // weak binding used here\n        .addListener((obs, old, current) -\u003e System.out.println(current));\n\nThis can be very surprising, especially when adding the `concat` function at a later stage,\nbecause that simple change will result in a completely different runtime behavior.\n\n## Motivation\n\nThis project was created in the hope to add additional functionality directly to JavaFX to address\na few of its rough edges. Mainly:\n\n- Type-safe `Bindings#select` functionality, which allows to create bindings to nested properties.  The current implementation\nis not type safe and does not offer much flexibility to customize the binding.\n\nThe project purposely contains only a small well defined subset of code adapted from ReactFX with the goal\nof making the project easier to evaluate for potential inclusion into JavaFX.  Direct inclusion would\noffer major advantages by adding default methods to the `ObservableValue` and `Binding` interfaces making\nclasses that implement these interfaces act more like `Optional`'s would:\n\n    Binding\u003cString\u003e quotedTitleText = model.titleProperty()\n        .map(text -\u003e \"'\" + text \"'\");  // new `map` method on `Binding`\n\n### Type-safe binding to nested properties\n\nIn standard JavaFX, creating a binding to a nested property is a cumbersome affair.  One has to keep\ntrack of the listeners to unregister them when a parent property changes, and reregister the listener\non the new value.  With multiple levels of nesting this can quickly become complicated and error prone.\n\nAn example from JavaFX itself is the implementation of the `treeShowing` property.  It tracks whether\nor not a `Node` is currently showing on the screen.  In order to do this, it must check if the `Node`\nhas a `Scene`, whether the `Scene` has a `Window`, and whether that `Window` is currently shown:\n\n        ChangeListener\u003cBoolean\u003e windowShowingChangedListener = (win, oldVal, newVal) -\u003e updateTreeShowing();\n\n        ChangeListener\u003cWindow\u003e sceneWindowChangedListener = (scene, oldWindow, newWindow) -\u003e {\n            if (oldWindow != null) {\n                oldWindow.showingProperty().removeListener(windowShowingChangedListener);\n            }\n            if (newWindow != null) {\n                newWindow.showingProperty().addListener(windowShowingChangedListener);\n            }\n            updateTreeShowing();\n        };\n\n        ChangeListener\u003cScene\u003e sceneChangedListener = (node, oldScene, newScene) -\u003e {\n            if (oldScene != null) {\n                oldScene.windowProperty().removeListener(sceneWindowChangedListener);\n\n                Window window = oldScene.windowProperty().get();\n                if (window != null) {\n                    window.showingProperty().removeListener(windowShowingChangedListener);\n                }\n            }\n            if (newScene != null) {\n                newScene.windowProperty().addListener(sceneWindowChangedListener);\n\n                Window window = newScene.windowProperty().get();\n                if (window != null) {\n                    window.showingProperty().addListener(windowShowingChangedListener);\n                }\n            }\n\n            updateTreeShowing();\n        };\n\nThis can already be expressed much more succintly by using the `Bindings#select` function:\n\n        BooleanProperty treeShowing = Bindings.selectBoolean(node.sceneProperty(), \"window\", \"showing\");\n\nThe method however is not type safe.  A mistake in one of the string parameters or the choice of select\nmethod will lead to errors at runtime.  It will also complain about `null` values and map them to some\nstandard value.\n\n#### Alternative solution using Streams\n\nWith streams we can create the same binding in a type-safe manner:\n\n        Binding\u003cBoolean\u003e treeShowing = Values.of(node.sceneProperty())\n            .flatMap(s -\u003e Values.of(s.windowProperty()))\n            .flatMap(w -\u003e Values.of(w.showingProperty()))\n            .orElse(false)\n            .toBinding();\n\nThis is far less cumbersome and still 100% type safe.\n\n### Preventing memory leaks\n\nWhen you bind a property in JavaFX you have to carefully consider the lifecycle of the two properties\ninvolved. Calling `bind` on a property will keep a target property synced with a source property.\n\n    target.bind(source);  // keep target in sync with source\n\nThis is equivalent to adding a (weak) listener (weak listener code omitted here) and keeping\ntrack of the property target was bound to:\n\n    source.addListener((obs, old, current) -\u003e target.set(current));\n    target.getProperties().put(\"boundTo\", source);\n\nIn both these cases:\n\n- source refers to target through the listener added because it needs to update the target when it changes\n- target refers to source as the property it is \"bound to\" in order for `unbind` to do its magic\n\nIn JavaFX, `bind` will use a weak listener, which means that the target can be garbage collected independently\nfrom the source property.  However, the reference from target to source with its \"bound to\" property is a hard\nreference (if it were weak then a binding could stop working without notice because the source could be \ngarbage collected and stop sending its updates).  This means that the lifecycle of the source property is\nnow closely tied to the target property.\n\nIf the target property is a long-lived object (like a data model) and the source property is a shorter lived\nobject like a UI element, you could have inadvertently created a big memory leak; all UI components\nhave a parent and a scene property, so effectively keeping a reference to any UI element can keep an entire\nscene or window from being garbage collected.\n\nSomething as simple as keeping the selection of a `ListView` in sync with a model can lead to this:\n\n    model.selectedItemProperty()\n        .addListener((obs, old, current) -\u003e listView.getSelectionModel().select(current));\n\nAssuming `model` here is a long-lived object that perhaps is re-used next time the UI is shown to remember\nthe last selected item, the listener as shown above will prevent the `ListView` and all other UI components\nthat it refers to from being garbage collected.\n\nTo prevent this one must remember to wrap the above listener in a `WeakChangeListener` and be careful not\nto keep a reference around to the unwrapped change listener.  Using a weak listener is not a perfect\nsolution however.  The listener only stops working when a garbage collection cycle runs and in the mean\ntime the UI code may interfere with normal operations if the selected item is changed in the model, as it\ncould trigger code in the (soon to be garbage collected) UI, which may still trigger other changes.\n\nIt might be better to disable the listener as soon as the UI is hidden.  Doing this manually means keeping\ntrack of the listeners involved that may need unregistering (and potentially reregistering if the UI becomes\nvisible again).  Instead we could listen to a property that tracks the showing status of our UI.\nUnfortunately, this is somewhat involved as there is no easy property one can listen to; you have to listen\nfor `Scene` changes, check which `Window` it is associated with and then bind to  `Window::showingProperty`\n-- and update these listeners if the scene or window changes.\n\nWith Streams one could safely bind a UI property to a model only when the UI is visible:\n\n    model.selectedItemProperty()\n        .conditionOn(isShowing)\n        .subscribe(selectedItem -\u003e listView.getSelectionModel().select(selectedItem));\n\nWhere the `isShowing` variable can be created like this:\n\n    Binding\u003cBoolean\u003e isShowing = Values.of(listView.sceneProperty())\n        .flatMap(s -\u003e Values.of(s.windowProperty()))\n        .flatMap(w -\u003e Values.of(w.showingProperty()))\n        .orElse(false)\n        .toBinding();\n\nOr with a small helper class, which only needs the `Node` involved as a parameter:\n\n    model.selectedItemProperty()\n        .conditionOn(Helper.isShowing(listView))\n        .subscribe(selectedItem -\u003e listView.getSelectionModel().select(selectedItem));\n\nThe above binding to `model.selectedItemProperty()` will only be present while `listView` is\nshowing.  If the list view is hidden, the listener is unregistered, and if it is shown again\nthe listener is re-added.  If the UI is hidden, it will instantly stop reacting to any \nchanges in the model and (if also no longer referenced) will eventually be garbage collected.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhjohn%2Fhs.jfx.eventstream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhjohn%2Fhs.jfx.eventstream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhjohn%2Fhs.jfx.eventstream/lists"}