FXML support
manuel-mauky opened this issue · comments
Hi Michael,
at javaland we have already discussed about how we could add FXML support to ReduxFX. I've worked on a prototype that is inspired by the angular implementation of Redux.
I've tried to get this working with your existing ReduxFX codebase but I had a hard time to get it working so instead I created the prototype in a separate repository: https://github.com/lestard/redux-javafx-example.
I hope that we can integrate this into your ReduxFX project at some point.
Separating Redux and JavaFX
In my prototype I've created a simple Redux implementation that is completely separated from any JavaFX specific code: https://github.com/lestard/redux-javafx-example/blob/master/src/main/java/redux/ReduxStore.java
It takes the initial state and a reducer. Like with the original redux you have a dispatch method that is used to dispatch actions and you can add subscribers that are invoked when the store is updated.
In the example I was creating the Views with standard JavaFX classes and FXML. My "Controller"-classes that are backing the FXML files implement an interface View which provides 2 features:
- a
dispatch
method that can be used to dispatch new actions - some
select
methods. This is what I've taken from Angular-Redux.
The select
method takes a "Selector" (a Function that takes the State as argument and returns a specific slice or value from this state) and returns an ObservableValue
or an ObservableList
.
Everytime the store gets updated these observables will get the new values provided by the selector.
This way you can bind your JavaFX Controls to the state of the Redux store.
This can look like this:
public class ControlsView implements View {
@FXML
public Label itemsLeftLabel;
public void initialize(){
itemsLeftLabel.textProperty().bind(
Bindings.concat(select(Selectors.numberOfItemsLeft), " items left"));
}
public void all() {
dispatch(Actions.setFilter(Filter.ALL));
}
public void active() {
dispatch(Actions.setFilter(Filter.ACTIVE));
}
public void completed() {
dispatch(Actions.setFilter(Filter.COMPLETED));
}
}
It means that all UI components are stateful. I know that this is not as clean and elegant as your VNode approach but this way we can use FXML.
ListViews
As you've already mentioned at javaland the handling of ListViews is problematic. I've had this problem already for FluxFX and I've copied the solution from there. It's solved with a special CellFactory that updates items when the underlying data has changed. This is also not a really clean solution but at least it works ;-) Maybe we can find a better approach. As I've said, it's all just a prototype.
Open questions
I hope you find the time to take a look at the prototype.
Maybe we could change the API of ReduxFX so that it's not coupled to your VNode approach only.
At the moment we have this code to startup ReduxFX:
ReduxFX.start(initialState, Todos::update, MainView::view, primaryStage);
In my example I've come up with this API:
ReduxStore<AppModel> store = ReduxStore.create(initialState, Updater::update);
ReduxJavaFX.connect(store);
This separates the Redux Store and the UI part. This is similar to redux and react-redux.
Maybe we could do the same for the Java(FX) library: A Core Redux library and a separate module with your VSceneGraph and another module with the FXML approach?
Bonus Middleware
While developing I've got the need for debugging and implemented a Logging-Middleware, inspired by the middleware concept of the original Redux implementation. Maybe this sort of extension mechanism could be added to ReduxFX too.
kind regards,
Manuel
Hi Manuel,
thanks a lot for your input. These are great ideas and I am looking forward to integrate them into ReduxFX.
I did not have time to look into the code yet, but hopefully I will be able to during the weekend. But in general I think it is a good pragmatic approach to add a Controller that links the state and the dispatcher to FXML.
Do you have any experience with bindings defined directly in FXML? As we do not need bidirectional bindings, I wonder if we could link the state to the FXML elements directly. Is it possible to define bindings in the JavaFX Scene Builder?
I like your ideas about separating the store and the UI and the middleware and will start to think about them immediately. This could actually open up ReduxFX for all kind of UI toolkits.
Once again, thanks a lot for your input and will get back once I had a chance to look at your prototype in more depth.
Cheers,
Michael
I haven't used bindings directly in FXML and as far as I know this isn't possible in SceneBuilder.
I think it's ok to have the controller class because it's well known for many JavaFX developers but using the direct binding in FXML is an interesting option to check in the future.
What I have found in the meantime is that there are already a redux api and implementation for Java on github. I haven't tested them yet but I will try to port my example to these libs to see how it works.
As far as I can see they are mostly targeted towards Android developers but we could use them for JavaFX as well and this way profit from contributers from the Android community.
I have the feeling that your ReduxFX library is for JavaFX what "React" is for DOM/JavaScript. A library to create components in a functional way including a VirtualDOM/Virtual SceneGraph.
The redux aspect is kind of optional. If you would extract the virtual SceneGraph part into a separate module one could use it together with redux or (for example) FluxFX or even other libraries.
I took a look at jvm-redux-api, but I do not think it is a good base. The idea seems to be to stick as close as possible to the original Redux. Unfortunately this means, it has the same shortcomings and it does not really take advantage of the possibilities of the JVM.
IMO from Java 9 on forward, Flow should be the base for how we connect reactive components (and org.reactivestreams until then). Therefore if the VirtualScenegraph is separated from the rest of ReduxFX, I'd prefer an API based on Flow to connect both parts.
This would be ok for me. I don't insist on using an existing API/library. I'm ok with implementing a new API when there are actual advantages.
I didn't know the Flow class and API but it looks promising.
I think the simplest solution would be that ReduxFXStore implements Processor. The API would look something like this:
public class ReduxFXStore<ACTION, STATE> implements Processor<ACTION, STATE> { ... }
Another option would be to keep input- and output stream separated, e.g.
public class ReduxFXStore<ACTION, STATE> {
public Subscriber<ACTION> getActionSubscriber() { ... }
public Publisher<STATE> getStatePublisher() { ... }
}
The View-component would have two connect-methods:
public class ReduxFXView<ACTION, STATE> {
public void connect(Processor<ACTION, STATE> store) { ... }
public void connect(Subscriber<ACTION> actionStream, Publisher<STATE> statePublisher) { ... }
}
What do you think?
Looks good for me. I would chose the Processor
interface.
I've updated the prototype here with the reactivestreams API but I don't know if I fully understood the life cycle and all methods of the API (especially the Subscription
part).
I'm not sure whether it's a good idea to have a generic type for ACTION
or to keep it as Object
.
At first I was using a marker interface for all actions but as soon as I started working on a thunk-middleware this wasn't working anymore because "actions" now could have been functions instead of plain objects. So I was using Object
instead of a generic type parameter for actions for the first attempt.
Ahhh, I always forget: Subscribers cannot be reused. So I guess the ReduxFXStore needs a method, that creates a new Subscriber on each call:
public Subscriber<ACTION> createActionSubscriber() { ... }
"...that creates a new Subscriber on each call"
on which call?
This is my current understanding:
The Store is a Publisher
and you can add your own Subscriber
to be notified about state changes.
This is done by keeping a list of Subscribers internally and invoking onNext(state)
on each of them after the reducer has finished.
The Store is a Subscriber
and you can notify it about new Actions by invoking onNext(new WhatEverAction())
. This triggeres the update cycle.
When a UI is "connected" to the Store a new Subscriber
is created and added to the Store.
Unfortunately Subscribers are slightly more complex. You subscribe them by calling Publisher.subscribe(). Once that happens onSubscribe() of the Subscriber is called with a Subscription as a parameter. The Subscriber signals to the Publisher that it is ready to receive x new events by calling Subscription.request(x). Sounds probably more complicated than it is. :)
Usually you do not deal with these details, because implementations like RxJava take care of it. This is another good argument for keeping the actionSubscriber and the store separated, because it allows you to use one of the implementations of Subscriber.
The documentation is not 100% clear to me, but from my understanding Subscribers must not be reused. This is no problem, if you have just one source of actions. But in ReduxFX you can attach Drivers to deal with side effects. Drivers receive Commands from the application and can send Actions back to the application. Therefore ReduxFX needs to generate several subscribers, one for each action-source.
I split store- and view-component and pushed everything to the branch 'reduxfx_split'.
I added two examples to demonstrate using the store- and the view-parts of ReduxFX in isolation. The example external-store uses the redux java library for the store together with the ReduxFX-View components. The fxml example is a simplified version of your counter-example. It uses the ReduxFX-store without the view components.
Both are working fine, therefore I pushed all the changes to master.