Elegant JavaFX
Using object-oriented design tactics and practices, you can make a simple, robust solution for creating and launching windows in a JavaFX app.
Join the DZone community and get the full member experience.
Join For FreeIn this post, I would like to introduce a simple JavaFX application that provides an elegant way to create and launch windows. I came up with this approach during one of my JavaFX projects, and I would like to share it with you. My assumption for this post is that the reader knows basics of JavaFX.
Simple Example
For the purposes of this post, I created an app with two independent windows: start and about. To achieve that, I need two different FXML's to describe the UI: start.fxml and about.fxml. Both FXMLs are very simple. The start window contains one button, which opens the about window. The about window contains a label with some text. To describe some actions for each window, I need controllers: StartController and AboutController. These controllers are concrete implementations of an AbstractController.
public abstract class AbstractController implements Initializable {
protected final ViewHandler viewHandler;
public AbstractController(ViewHandler viewHandler) {
this.viewHandler = viewHandler;
}
@Override
public abstract void initialize(URL location, ResourceBundle bundle);
}
All JavaFX controllers implement Initializable interfaces, so AbstractController does it as well. This interface has one method, initialize(...), with two parameters. The first is the URL to the FXML file, and the second is a resource bundle that contains all text for the UI. I made initialize(...) abstract so concrete classes could implement it in their own ways. AbstractController contains one field: viewHandler. Before I explain what this handler does, let's see the implementation of StartController and AboutController:
public class StartController extends AbstractController {
@FXML private Button aboutButton;
public StartController(ViewHandler viewHandler) {
super(viewHandler);
}
@Override
public void initialize(URL location, ResourceBundle resources) {
aboutButton.setOnAction(aboutAction());
}
private EventHandler<ActionEvent> aboutAction() {
return e -> {
try {
viewHandler.launchAboutWindow();
} catch (IOException ex) {
/* implementation of alert dialog */
}
};
}
}
Because the start window contains a button that opens about, the aboutButton has to be added to the controller. Also, the action for our aboutButton has to be implemented. The interesting part for this piece of code is done in aboutAction(). Here, the action is delegated to viewHandler, which launches the about window. AboutController is very simple:
public class AboutController extends AbstractController {
public AboutController(ViewHandler viewHandler) {
super(viewHandler);
}
@Override
public void initialize(URL location, ResourceBundle bundle) {
/*Some initialization here*/
}
}
Now let's see what the interface ViewHandler actually does. This interface is responsible for launching windows. Basically, it's one interface to rule them all. In our case, it contains only two methods:
public interface ViewHandler {
void launchStartWindow() throws IOException;
void launchAboutWindow() throws IOException;
}
Let's see the implementation of this interface:
public class AppViewHandler implements ViewHandler {
private final Stage primaryStage;
private final ResourceBundle bundle;
public AppViewHandler(Stage primaryStage, ResourceBundle bundle) {
this.primaryStage = primaryStage;
this.bundle = bundle;
}
@Override
public void launchStartWindow() throws IOException {
buildAndShowScene(primaryStage, WindowFactory.START.createWindow(this, bundle));
}
@Override
public void launchAboutWindow() throws IOException {
Stage aboutStage = new Stage();
aboutStage.initModality(Modality.WINDOW_MODAL);
buildAndShowScene(aboutStage, WindowFactory.ABOUT.createWindow(this, bundle));
}
private void buildAndShowScene(Stage stage, AbstractWindow window) throws IOException {
try (InputStream is = getClass().getClassLoader().getResourceAsStream(window.iconFilePath())) {
stage.getIcons().add(new Image(is));
}
stage.setTitle(bundle.getString(window.titleBundleKey()));
stage.setResizable(window.resizable());
stage.setScene(new Scene(window.root()));
stage.show();
}
The AppViewHandler class contains two fields: primaryStage and bundle. The field primaryStage of type Stage is the foundation upon which our start window is built.
The bundle field contains the resource bundle for our application. Both fields are final and initialized in the constructor.
The launchStartWindow method is responsible for launching the start view. But before it launches the window, it has to build it first. For that, the method buildAndShowScene is introduced. Actually, these methods are very interesting. It takes two parameters: stage, upon which the window is built, and window. Stage is a JavaFX type that the user is able to see as a classic window on the screen. In these methods, stage sets some properties such as: icon, title, scene, and information about whether the user is able to change the size of the window.
As you can see, all that information is taken from the parameter window of type AbstractWindow. I will describe AbstractWindow later in this post. Now let's go back to launchStartWindow and see what is going on there. The most important thing here is WindowFactory. This is my version of a factory, which creates a proper window. It is an enum. I will describe it later in this post. Now let's see where AppViewHandler is created.
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
new AppViewHandler(primaryStage, ResourceBundle.getBundle(AppPaths.RESOURCE_BUNDLE, Locale.getDefault()))
.launchStartWindow();
}
public static void main(String[] args) {
launch(args);
}
}
The Main class is a starting point for this application. This class is a standard JavaFX start class. AppViewHandler is created in the start method. First, a constructor with two parameters is called. These parameters are primaryStage and resource bundle. After constructing the object, the launchStartWindow method is called.
Now let's see the WindowFactory enum:
public enum WindowFactory {
START {
@Override
public AbstractWindow createWindow(ViewHandler viewHandler, ResourceBundle bundle) {
return new StartWindow(new StartController(viewHandler), bundle);
}
},
ABOUT {
@Override
public AbstractWindow createWindow(ViewHandler viewHandler, ResourceBundle bundle) {
return new AboutWindow(new AboutController(viewHandler), bundle);
}
};
public abstract AbstractWindow createWindow(ViewHandler viewHandler, ResourceBundle bundle);
}
Because this app contains only two views, you can see two elements here. This enum also contains one public abstract method, createWindow(), which returns AbstractWindow. The method gets two parameters that are required to build windows. Each element of the enum has unique implementations of this method. That is how the factory creates the proper object for the window.
The first element is START, which is responsible for creating the start window. The createWindow() method of the START element creates a StartWindow object. StartWindow needs a proper controller and resource bundle. The same case goes for the ABOUT element.
Someone could ask, “Why do you create a controller for the window here. It would be simpler to just pass viewHandler and create a controller inside window, considering that the view must have its own controller.” My answer is: AbstractWindow should not contain any information about ViewHandler because it would be a layer violation. The relationship is the other way around. ViewHandler handles views. AbstractWindow contains information only about windows.
And now is a perfect time to introduce AbstractWindow.
public abstract class AbstractWindow {
private final AbstractController controller;
private final ResourceBundle bundle;
public AbstractWindow(AbstractController controller, ResourceBundle bundle) {
this.controller = controller;
this.bundle = bundle;
}
public Parent root() throws IOException {
FXMLLoader loader = new FXMLLoader(url(), bundle);
loader.setController(controller);
return loader.load();
}
private URL url() {
return getClass().getClassLoader().getResource(AppPaths.FXML_PATH + fxmlFileName());
}
public String iconFilePath() {
return AppPaths.IMG_PATH + iconFileName();
}
public boolean resizable() {
return false;
}
protected abstract String iconFileName();
protected abstract String fxmlFileName();
public abstract String titleBundleKey();
}
AbstractWindow is an abstraction that contains all needed details for windows. Some of these properties were mentioned earlier. This class has information about the name of the FXML file, the path of the icon file, the title bundle key, root, and resizing. Most of this information is unique for each window, but some are not. All unique information is defined in concrete implementations. Now let's see StartWindow and AboutWindow:
public class StartWindow extends AbstractWindow {
public StartWindow(AbstractController controller, ResourceBundle bundle) {
super(controller, bundle);
}
@Override
protected String iconFileName() {
return "startIcon.png";
}
@Override
protected String fxmlFileName() {
return "start.fxml";
}
@Override
public String titleBundleKey() {
return "start.title";
}
}
public class AboutWindow extends AbstractWindow {
public AboutWindow(AbstractController controller, ResourceBundle bundle) {
super(controller, bundle);
}
@Override
protected String iconFileName() {
return "aboutIcon.png";
}
@Override
protected String fxmlFileName() {
return "about.fxml";
}
@Override
public String titleBundleKey() {
return "about.title";
}
}
As you can see, all unique information about our windows is defined inside these classes.
Summary
Above, I've shown an example of OO design for JavaFX apps. I hope you will find this post interesting. You can find the full example on my GitHub. Please let me know in the comments what you think about it. I hope it will be useful.
Opinions expressed by DZone contributors are their own.
Comments