Implement your own Redux in Java. Part 2: Simple FXML Views
javareduxIn this series of blog posts I'm implementing my own Redux in Java to use with JavaFX.
However, the purpose is not to create a production-ready library (because there is already ReduxFX available) but instead to learn by re-implementing it.
In the previous part I've showed how the basic Redux functionality can be implemented. In this part I will show how to use it with JavaFX and how to combine a simple FXML based JavaFX View with our Redux store.
Topics in this blog series (maybe I will adjust this in the process of writing):
- Part 1: Basic Redux functionality
- Part 2: Simple FXML Views
- Part 3: First approach on Middlewares
- Part 4: Improved Middleware API
- Part 5: Async behavior with Middlewares
- Part 6: Other examples for Middlewares
- Store-enhancers
- Dev-Tools with time-travel-debugging
- advanced FXML views
- declarative JavaFX Views
- ReduxFX - an actual library
The UI
A typical JavaFX UI consists of a FXML file and a controller class. This can look like this:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox fx:controller="example.TodoViewController" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<HBox spacing="5.0">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
<TextField fx:id="input" HBox.hgrow="ALWAYS"/>
<Button mnemonicParsing="false" text="Add" onAction="#addItem"/>
</HBox>
<ListView fx:id="items" VBox.vgrow="ALWAYS"/>
</VBox>
package example;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
public class TodoViewController {
@FXML
private ListView items;
@FXML
private TextField input;
public void addItem() {
// todo implement
}
}
Now we need to solve two problems: 1) we need to create and dispatch a new action when the add-button is clicked and 2) we need to get the data from the store and update our UI when the data has changed. The easiest way to solve this is to give the controller access to our Redux store. This is a typical use-case for dependency-injection and for this example I'm using my own dependency-injection library Easy-DI. This way we can inject an instance of the Redux store into the controller. The setup code in the main app class looks like this:
public void start(Stage primaryStage) throws Exception {
// create dependency injection context
EasyDI context = new EasyDI();
// create the initial state with an empty item list
TodoState initialState = new TodoState(Collections.emptyList());
// create the store by passing the initial state and the reducer to the constructor
Store<TodoState> store = new Store<>(initialState, new TodoReducer());
// configure the DI context so that the store instance can be injected
context.bindInstance(Store.class, store);
// the location of the fxml file to load
final URL location = this.getClass().getResource("TodoView.fxml").toURI().toURL();
FXMLLoader fxmlLoader = new FXMLLoader(location);
// tell JavaFX how to get new instances from the DI context
fxmlLoader.setControllerFactory(context::getInstance);
final VBox parent = fxmlLoader.load();
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
EasyDI only supports constructor-injection but if you are using other DI frameworks maybe you can also use field injection. Our controller now looks like this:
public class TodoViewController {
@FXML
private ListView items;
@FXML
private TextField input;
private final Store<TodoState> store;
public TodoViewController(Store<TodoState> store) {
this.store = store;
}
public void addItem() {
// todo implement
}
}
Dispatching actions
Now that we have access to the store it's easy to dispatch new actions with the dispatch
method:
public void addItem() {
final String text = input.getText();
Action action = new AddItemAction(text);
store.dispatch(action);
input.setText("");
}
Update the UI on state changes
With the subscribe
method we have a way to get notified about state changes.
public TodoViewController(Store<TodoState> store) {
this.store = store;
store.subscribe(newState -> {
items.getItems().setAll(newState.getItems());
});
}
For this small example updating the UI is easy. We throw away the existing items in the JavaFX ListView
and overwrite
them with the new ones. However, for more complex use-cases and UIs it can become really tricky. This style of
development can be a problem because JavaFX itself is not optimized for this kind of usage. The reason for this is that
JavaFX has an imperative API - at least when you compare it to the fully declarative API of React.js for which the
original Redux was invented. In a future post I will go into more detail what I mean with "fully declarative API" and
why in my opinion JavaFX is not fully declarative. And I will also show an alternative declarative and functional API
for JavaFX that is more similar to react.js and is a good fit for a Redux based app.
However, even with the "normal" FXML based approach you can create good UI's with Redux. Last year I have written some useful helpers to support the combination of FXML and Redux as a contribution to the ReduxFX project. In one of the next blog posts I will give a more detailed explanation of these tools and show some patterns for Redux+FXML.
You can find the code for redux example in this github repository. This commit is pointing directly to the state of this blog post.