Build a JavaFX native executable with FXML

This post is part of the 100 Days of JavaFX Series.

So far we were able to build a native executable for our Java application using the Client Maven Plugin from Gluon. We built the UI using Java code only. What if we want to make use of more “dynamic” features such as FXML?

We will also cover the shortcomings you may face when using FXML with the Java Module System.

Preparation

If you don’t have one already, create a resources folder in your Maven project:

cp -r hellofx hellofxml
cd hellofxml
mkdir -p src/main/resources/

In order to use FXML we will need to add dependencies to:

  1. Our Maven Project (pom.xml)
  2. Our Java Module (module-info.java)
<!-- pom.xml -->

<properties>
  <javafx.version>15</javafx.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>${javafx.version}</version>
  </dependency>
  <dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>${javafx.version}</version>
  </dependency>
</dependencies>
// src/main/java/module-info.java

module eu.leward.hellofxml {
    requires javafx.controls;
    requires javafx.fxml;

    opens eu.leward.hellofxml to javafx.graphics;
}

FXML

FXML is a markup language derivative from XML used to describe User Interfaces for JavaFX. It is similar to:

  • XAML in .Net
  • XML Layouts for Android
  • HTML for the web (to some extent: HTML is less strict than XML regarding the validity of the markup)

Consider the following UI we built previously:

public class HelloFXML extends Application {
    @Override
    public void start(Stage stage) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
        Scene scene = new Scene(new StackPane(l), 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

With FXML, defining the UI looks like this:

<!-- An FXML file is an XML file -->
<?xml version="1.0" encoding="UTF-8"?>

<!-- Imports are like Java Import, they allow you use classes without
having to specify there fully qualified name.
Here, the imports were auto-generated by my IDE (IntelliJ). -->
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<!-- Stack Pane is the root node -->
<StackPane xmlns="http://javafx.com/javafx"
           xmlns:fx="http://javafx.com/fxml"
           prefWidth="640.0" prefHeight="480.0">

    <Label text="Hello ${er} JavaFX!"/>

</StackPane>

Create this file as src/main/resources/hello.fxml.

To use the FXML file you will need to use a FXMLLoader. Its job is to load a FXML resource and use in Java code as a regular JavaFX UI Node. The type you will be loading is the top element in the FXML file. In our example, it is a StackPane.

public class HelloFXML extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        // Load the FXML
        URL fxmlResource = HelloFXML.class.getResource("/hello.fxml");
        FXMLLoader loader = new FXMLLoader(fxmlResource);
        StackPane pane = loader.load();

        // Assign the loaded view to the stage and show it
        Scene scene = new Scene(pane);
        stage.setScene(scene);
        stage.show();

    }

    public static void main(String[] args) {
        launch();
    }
}

And running it a nice “Hello JavaFX” window:

Hello World JavaFX App

However, our text is no longer dynamic: it does not show which version of Java and JavaFX we are running.

That is the main drawback a FXML: its static nature. However there are ways to make it more dynamic while keeping a static markup.

Introducing a Controller

You can link together the FXML with some Java Code by setting a Controller on your root node:

<StackPane xmlns="http://javafx.com/javafx"
           xmlns:fx="http://javafx.com/fxml"
           fx:controller="eu.leward.hellofxml.HelloController"
           prefWidth="640.0" prefHeight="480.0">

    <Label text="Hello JavaFX!"/>

</StackPane>

With an empty HelloController class:

package eu.leward.hellofxml;

public class HelloController {
}

Trying to run the application now will result in Illegal Access Exception.

Caused by: java.lang.IllegalAccessException: 

class javafx.fxml.FXMLLoader$ValueElement (in module javafx.fxml) 
  cannot access class eu.leward.hellofxml.HelloController (in module eu.leward.hellofxml) 
  because module eu.leward.hellofxml does not export eu.leward.hellofxml to module javafx.fxml

Because we use Java module system, we need to allow the javafx.fxml to use our classes.

This can be fixed by adding an exports instruction in the [module-info.java](http://module-info.java) file:

// src/main/java/module-info.java

module eu.leward.hellofxml {
    requires javafx.controls;
    requires javafx.fxml;

    opens eu.leward.hellofxml to javafx.graphics;
    exports eu.leward.hellofxml to javafx.fxml;
}

Using Bindings with the Controller

We now have a controller linking our FXML root element with some Java code but this code is not doing anything for now.

Let’s replicate the example we had before switching to FXML.

From the controller, we are able to interact with UI elements in the FXML. In order to do so:

  1. the elements must have an identifier, an fx:id attribute
  2. the controller must have a binding on that identifier using an attribute annotated with @FXML. The attribute have to match the ID used in the FXML.
<Label fx:id="label" text="Hello JavaFX!"/>
package eu.leward.hellofxml;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class HelloController {
    @FXML
    private Label label;
}

Now, you should be running into another error running this snipet:

Unable to make field private javafx.scene.control.Label eu.leward.hellofxml.HelloController.label accessible: 
module eu.leward.hellofxml does not "opens eu.leward.hellofxml" to module javafx.fxml

Before we are using private attributes, the javafx.fxml module needs to be able perform reflection access on our code. Using exports in module-info.java is not enough, we need to use open instead:

// src/main/java/module-info.java

module eu.leward.hellofxml {
    requires javafx.controls;
    requires javafx.fxml;

    opens eu.leward.hellofxml to javafx.graphics, javafx.fxml;
}

In JavaFX you can define an initialize() method annotated with @FXML. It will get executing when initializing the controller. This is called after the @FXML bindings are applied, meaning you can interact with the UI nodes in this method.

package eu.leward.hellofxml;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class HelloController {
    @FXML
    private Label label;

    @FXML
    public void initialize()
    {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        label.setText("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
    }
}
Hello World JavaFX App

The we have the text from the original example. The example is simple, but it is good introduction to the concept of FXML markup and controller.

Compile to a native binary

Let’s now try to compile the code as a native binary with mvn client:build

It takes some time but the builds looks good:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:56 min
[INFO] Finished at: 2020-11-08T00:21:46-05:00
[INFO] ------------------------------------------------------------------------

However, running the application, we have a nasty ClassNotFoundException.

target/client/x86_64-linux/hellofx

...

Exception in thread "main" java.lang.RuntimeException: Exception in Application start method
(...)
Caused by: java.lang.ClassNotFoundException: eu.leward.hellofxml.HelloController

We are getting this error, because the compiler only keeps the classes which it knows are effectively used. However because the controller is only used in an .fxml file, the compiler thinks HelloController is an orphaned class and therefore should be ignored.

Fortunately the gluon plugin has a configuration option for that: reflectionList. We can to that list the classes we want to compiler to keep.

<!-- pom.xml -->

<plugin>
  <groupId>com.gluonhq</groupId>
  <artifactId>client-maven-plugin</artifactId>
  <version>0.1.31</version>
  <configuration>
    <mainClass>eu.leward.hellofxml.HelloFXML</mainClass>
    <reflectionList>
      <list>eu.leward.hellofxml.HelloController</list>
    </reflectionList>
  </configuration>
</plugin>

Re-compile and run…

Hello World JavaFX App

Hurray, it’s working just how we want it 🙂!