Files
fxml-compiler/README.md

9.3 KiB

FXML Compiler

Introduction

This projects aims at generating Java code from FXML files.

Requirements

  • Java 21 (at least for the plugin, the generated code can be compatible with older Java versions)
  • Maven 3.8.0

Installation

Add the plugin to your project:


<build>
    <plugins>
        <plugin>
            <groupId>com.github.gtache</groupId>
            <artifactId>fxml-compiler-maven-plugin</artifactId>
            <version>1.0.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Optionally add dependencies to the plugin (e.g. when using MediaView and controlsfx):


<build>
    <plugins>
        <plugin>
            <groupId>com.github.gtache</groupId>
            <artifactId>fxml-compiler-maven-plugin</artifactId>
            <version>1.0.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.openjfx</groupId>
                    <artifactId>javafx-media</artifactId>
                    <version>${javafx.version}</version>
                </dependency>
                <dependency>
                    <groupId>org.controlsfx</groupId>
                    <artifactId>controlsfx</artifactId>
                    <version>${controlsfx.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Advantages

  • Compile-time validation
  • Faster startup speed for the application
  • Possibility to use controller factories to instantiate controllers with final fields
  • Easier time with JPMS
    • No need to open the controllers packages to javafx.fxml
    • No need to open the resources packages when using use-image-inputstream-constructor (if images or resource bundles are in the project resources)

Disadvantages

  • Possible bugs (file an issue if you see one)
  • Expression binding is limited
  • Probably not fully compatible with all FXML features (file an issue if you need one in specific)

Parameters

Field injection

There are four ways to inject fields into a controller:

  • REFLECTION: Inject fields using reflection (like FXMLLoader)
    • try {
        final var field = controller.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(controller, object);
      } catch (final NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException("Error using reflection on " + fieldName, e);
      }
      
    • Slowest method
    • Fully compatible with FXMLLoader, so this allows easy switching between the two.
    • This is the default injection method (for compatibility reasons).
  • ASSIGN: variable assignment
    • controller.field = value
    • This means that the field must be accessible from the view (e.g. package-private).
  • SETTERS: controller setters methods
    • controller.setField(value)
  • FACTORY: controller factory
    • controller = factory.create(fieldMap)
    • factory is a ControllerFactory instance that is created at runtime and passed to the view.
    • fieldMap is a map of field name (String) to value (Object) that is computed during the view load method.
    • This allows the controller to have final fields.

Method injections

There are two ways to inject methods (meaning use them as event handlers) into a controller:

  • REFLECTION: Inject methods using reflection (like FXMLLoader)
    • try {
        final java.lang.reflect.Method method;
        final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods())
                .filter(m -> m.getName().equals(methodName)).toList();
        if (methods.size() > 1) {
            final var eventMethods = methods.stream().filter(m ->
                    m.getParameterCount() == 1 && javafx.event.Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList();
            if (eventMethods.size() == 1) {
                method = eventMethods.getFirst();
            } else {
                final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList();
                if (emptyMethods.size() == 1) {
                    method = emptyMethods.getFirst();
                } else {
                    throw new IllegalArgumentException("Multiple matching methods for " + methodName);
                }
            }
        } else if (methods.size() == 1) {
            method = methods.getFirst();
        } else {
            throw new IllegalArgumentException("No matching method for " + methodName);
        }
        method.setAccessible(true);
        if (method.getParameterCount() == 0) {
            method.invoke(controller);
        } else {
            method.invoke(controller, event);
        }
      } catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {
        throw new RuntimeException("Error using reflection on " + methodName, ex);
      }
      
    • Slowest method
    • Fully compatible with FXMLLoader, so this allows easy switching between the two.
    • This is the default injection method (for compatibility reasons).
  • REFERENCE: Directly reference the method
    • controller.method(event)
    • This means that the method must be accessible from the view (e.g. package-private).

Resource bundle injection

There are three ways to inject resource bundles into a controller:

  • CONSTRUCTOR: Inject resource bundle in the view constructor
    • view = new View(controller, resourceBundle);
      
    • This is the default injection method because it is the most similar to FXMLLoader ( FXMLLoader.setResources(resourceBundle)).
  • CONSTRUCTOR_FUNCTION: Injects a function in the view constructor
    • bundleFunction.apply(key)
    • The function takes a string (the key) and returns a string (the value)
    • This allows using another object than a resource bundle for example
  • GETTER: Retrieves the resource bundle using a controller getter method
    • controller.resources()
    • The method name (resources) was chosen because it matches the name of the field injected by FXMLLoader.
    • The method must be accessible from the view (e.g. package-private).
  • GET-BUNDLE: Injects the bundle name in the view constructor and retrieves it using ResourceBundle.getBundle(bundleName)
    • ResourceBundle.getBundle(bundleName)
    • Also used when fx:include specifies a resource attribute to pass it to the included view.

View creation

The views are generated in the same packages as the FXML files.
The name of the class is generated from the name of the FXML file.

The constructor of the view is generated depending on the parameters of the plugin.
The constructor will have as many arguments as the number of controllers in the FXML tree (recursive fx:include) + potentially the resource bundle if necessary. If no resource reference (%key.to.resource) is found in the FXML tree or if all the includes using references specify a resources attribute, the argument is not created.

The type of the constructor arguments will either be the controller instance or the controller factory (a function of fields map -> controller).
The resource bundle argument will either be the resource bundle instance, the resource bundle name or a function of string -> string.

The smallest constructor will have only one argument: The controller (or controller factory).

Maven Plugin

Parameters

  • output-directory
    • The output directory of the generated classes
    • default: ${project.build.directory}/generated-sources/java)
  • target-version
    • The target Java version for the generated code
    • default: 21
    • minimum: 8
    • File an issue if the generated code is not compatible with the target version
  • use-image-inputstream-constructor
    • Use the InputStream constructor for Image instead of the String (URL) one.
    • default: true
    • Disables background loading
  • field-injection
    • The type of field injections to use (see Field injection)
    • default: REFLECTION
  • method-injection
    • The type of method injections to use (see Method injection)
    • default: REFLECTION
  • bundle-injection
  • bundle-map
    • A map of resource bundle name to resource bundle path
    • Used with GET-BUNDLE injection
    • default: {}

Limitations

  • Given that the plugin operates during the generate-sources phase, it doesn't have access to the classes of the application.
    • The controller info (fields, methods) is obtained from the source file and may therefore be inaccurate.
    • Custom classes instantiated in the FXML files are not available during generation and may therefore cause it to fail.
  • If the application uses e.g. WebView, the javafx-web dependency must be added to the plugin dependencies.