Files
NeitzelLib/fx/docs/quick-start.md
T

9.1 KiB

Your first JavaFX application

This walkthrough creates a complete Maven project with one editable name and a label that follows it. It uses Java 21, JavaFX 21.0.7 and the current 1.0-SNAPSHOT NeitzelLib sources. These are local development artifacts; the instructions do not assume that this snapshot is published in Maven Central.

Prepare the library

Install a JDK 21. Use the library's Maven wrapper (3.9.11), or a separate Maven installation version 3.8.6 or newer. Check java -version and ./mvnw -version before building; on macOS, select the JDK with export JAVA_HOME=$(/usr/libexec/java_home -v 21) if necessary.

git clone https://gitea.neitzel.de/konrad/NeitzelLib.git
cd NeitzelLib
./mvnw -pl fx-example -am -Dmaven.test.failure.ignore=false install
./mvnw -f fx-example/pom.xml javafx:run

The install command installs the parent POM, core, injection and fx into your local Maven repository and builds the example. The repository hardcodes Surefire failure-ignore, so Maven success alone does not establish passing tests, even with the explicit flag. Inspect the XML reports as shown below. The javafx:run command opens the person desk example. Run javafx:run only on the example POM, not with -am: the reactor parent is not a JavaFX application.

Check test results after the build (requires Python 3):

python3 - <<'PYTHON'
from pathlib import Path
import xml.etree.ElementTree as ET
reports = list(Path('.').glob('*/target/surefire-reports/TEST-*.xml'))
assert reports, 'No test reports found'
failures = []
for report in reports:
    suite = ET.parse(report).getroot()
    if int(suite.get('failures', 0)) or int(suite.get('errors', 0)):
        failures.append(str(report))
assert not failures, 'Failed test reports: ' + ', '.join(failures)
print(f'{len(reports)} test suites passed')
PYTHON

Use a clean build when qualifying a change so stale reports cannot stand in for tests that did not run.

Create the application

Create a separate directory hello-fx with these files. This is a classpath application; it deliberately has no module-info.java.

hello-fx/
  pom.xml
  src/main/java/example/Main.java
  src/main/java/example/HelloApp.java
  src/main/java/example/Person.java
  src/main/resources/example/Hello.fxml

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>hello-fx</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>de.neitzel.lib</groupId>
      <artifactId>fx</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>2.0.17</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.1</version>
      </plugin>
      <plugin>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-maven-plugin</artifactId>
        <version>0.0.8</version>
        <configuration><mainClass>example.Main</mainClass></configuration>
      </plugin>
    </plugins>
  </build>
</project>

src/main/java/example/Person.java:

package example;

public final class Person {
    private String name = "Ada";

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

src/main/java/example/Main.java:

package example;

import javafx.application.Application;

public final class Main {
    public static void main(String[] args) {
        Application.launch(HelloApp.class, args);
    }
}

src/main/java/example/HelloApp.java:

package example;

import de.neitzel.fx.view.LoadedView;
import de.neitzel.fx.view.ViewLoader;
import de.neitzel.injection.ApplicationContext;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public final class HelloApp extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Person person = new Person();
        ApplicationContext context = new ApplicationContext("example");
        LoadedView view = ViewLoader.load(
                HelloApp.class.getResource("Hello.fxml"), person, context);
        stage.setScene(new Scene(view.getRoot()));
        stage.setTitle("Hello NeitzelLib");
        stage.show();
    }
}

src/main/resources/example/Hello.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import de.neitzel.fx.view.BindingList?>
<?import de.neitzel.fx.view.DirectBinding?>
<VBox xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
      spacing="12" prefWidth="360" prefHeight="120">
    <fx:define>
        <BindingList>
            <DirectBinding control="name" property="text" path="name" direction="both"/>
            <DirectBinding control="echo" property="text" path="name" direction="view"/>
        </BindingList>
    </fx:define>
    <TextField fx:id="name"/>
    <Label fx:id="echo"/>
</VBox>

From hello-fx, run:

mvn clean compile javafx:run

Without a separate Maven installation, use the absolute path to the cloned library's mvnw with -f /absolute/path/to/hello-fx/pom.xml clean compile javafx:run.

Type another name: the label follows immediately, and the Person setter receives the new value. No controller is required for this form. ViewLoader creates a runtime ViewModel that holds the JavaFX properties. Updating person.setName(...) directly later does not notify the ViewModel; update ((javafx.beans.property.StringProperty) view.property("name")).set("Grace") on the JavaFX application thread instead.

Add behavior and reusable forms

Use an injected controller for event handlers or Computed bindings, and a Component for a reusable FXML form. The person desk demonstrates both, converters, lists, tables and messages. Read the architecture guide before introducing nested Models or sharing windows: Model identity, controller lifetime and list notifications matter.

fx brings core, injection and JavaFX base, graphics, controls, FXML and web transitively. You do not need Lombok or a generated ViewModel to use this example. Java ViewModel generation and the Designer are optional tools, not prerequisites for loading an FXML view.

For a named-module application, see the module and lifecycle notes. Do not simply add module-info.java without opening the packages used through reflection.

Troubleshooting the first launch

Symptom Check
Maven cannot resolve de.neitzel.lib:fx:1.0-SNAPSHOT or its parent Install the library reactor first; use the same Maven local repository for both projects. These instructions use local snapshots.
No plugin found for prefix 'javafx' Invoke the example POM directly, or use the complete application POM above. Do not invoke the goal on the library reactor parent.
JavaFX runtime/native library failure Check the JDK version, architecture and JavaFX dependencies actually resolved; avoid mixing native artifacts from different platforms or JavaFX versions.
getResource(...) returns null The FXML belongs under src/main/resources in the matching package; verify it was copied to target/classes. Resource names are case-sensitive.
No component found for type Annotate the controller/converter/Component, include it in the context's scan root, and inspect ComponentScanner.getErrors() for unresolved dependencies.
Binding cannot find control/property/path Match fx:id, a public JavaFX nameProperty() target, and a readable/writable Model Attribute. Use ViewLoader, not plain FXMLLoader.
Initial data shows but a bean setter does not update the UI Update the observable ViewModel property; the bean is not itself observable.
A second window fails with an already-parented node Create a separate context for that window; avoid reusing cached Component roots and controllers.
Reflective access fails only on module path Add the appropriate opens declarations and verify registry service declarations, not only dependencies.

Do not build a single ordinary JAR and expect it to include JavaFX's platform-specific runtime automatically. The Maven run goal handles the development launch; deployment requires the application's dependencies or a separately built runtime image. The person desk's JAR-plus-libs packaging is an example of the former, and the Designer has its own bundled runtime packaging.