JavaFX Controls, Layouts & FXML

Controllers & @FXML

18 min Lesson 7 of 12

Controllers & @FXML

In the previous lesson you learned that an FXML file is a declarative description of a scene graph. By itself, though, a static scene does nothing — no button clicks, no data validation, no navigation. That interactive behaviour lives in a controller class: a plain Java class wired to the FXML file by the FXMLLoader. This lesson covers everything you need to build and connect a controller in production-quality JavaFX code.

The Role of a Controller

A controller is the MVC "C". It holds references to the widgets declared in FXML, responds to user events, and updates the model. It does not contain layout code — that is the job of the FXML file (and, optionally, Scene Builder).

The separation is deliberate and valuable: a designer can edit the FXML without touching Java; a developer can test the controller logic independently of the view.

Declaring a Controller in FXML

You associate a controller with an FXML file through the fx:controller attribute on the root element:

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.LoginController" spacing="12" alignment="CENTER" prefWidth="320" prefHeight="240"> <Label text="Username"/> <TextField fx:id="usernameField"/> <Label text="Password"/> <PasswordField fx:id="passwordField"/> <Button text="Log In" onAction="#handleLogin"/> <Label fx:id="errorLabel" style="-fx-text-fill: red;"/> </VBox>

Two attributes do the wiring work here:

  • fx:id — gives a widget a name that the FXMLLoader will inject into the matching field in the controller.
  • onAction="#handleLogin" — the # prefix means "call this method on the controller". It works for any event attribute (onKeyPressed, onMouseClicked, etc.).

Writing the Controller Class

The controller is a normal Java class. Fields that match fx:id values must be annotated with @FXML. The FXMLLoader uses reflection to inject them — it bypasses access modifiers, so fields can be private (and should be).

package com.example; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; public class LoginController { @FXML private TextField usernameField; @FXML private PasswordField passwordField; @FXML private Label errorLabel; /** * Called automatically by FXMLLoader after all @FXML fields are injected. * Use it instead of a constructor for any initialisation that needs the widgets. */ @FXML private void initialize() { errorLabel.setVisible(false); usernameField.requestFocus(); } @FXML private void handleLogin() { String user = usernameField.getText().trim(); String pass = passwordField.getText(); if (user.isEmpty() || pass.isEmpty()) { errorLabel.setText("Please fill in both fields."); errorLabel.setVisible(true); return; } // delegate to a service — keep controller thin boolean ok = AuthService.authenticate(user, pass); if (!ok) { errorLabel.setText("Invalid credentials. Try again."); errorLabel.setVisible(true); passwordField.clear(); } else { SceneRouter.goTo("dashboard"); } } }
Why initialize() and not a constructor? When the constructor runs, the FXMLLoader has not yet injected any fields. initialize() is called after all @FXML fields are populated, so it is the correct place to set default state, bind properties, or fetch initial data.

Loading the FXML from Java

An application entry point (or a scene-switching helper) uses FXMLLoader to parse the file and hand back the root node:

import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class App extends Application { @Override public void start(Stage stage) throws Exception { FXMLLoader loader = new FXMLLoader( getClass().getResource("/fxml/login.fxml") ); Parent root = loader.load(); // parses FXML, instantiates controller, injects fields stage.setScene(new Scene(root)); stage.setTitle("My App"); stage.show(); } }

After loader.load() returns you can call loader.getController() to obtain the controller instance. This is useful when you need to pass data into the controller before showing the scene — a common pattern for passing a model object to a detail screen.

FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/detail.fxml")); Parent root = loader.load(); DetailController ctrl = loader.getController(); ctrl.setItem(selectedItem); // pass data before the scene is shown stage.setScene(new Scene(root)); stage.show();

Passing Data Between Controllers

There is no built-in "router" in JavaFX. Common patterns for transferring data when navigating between screens:

  1. Setter injection (shown above) — the calling code calls a public setter on the next controller after load() but before show().
  2. Shared model / service singleton — both controllers hold a reference to the same service or observable model object; the second controller reads it in initialize().
  3. Constructor injection via a controller factory — pass a lambda as the second argument to the FXMLLoader constructor to construct controllers yourself, enabling proper dependency injection.
// Controller factory — lets you inject dependencies via constructor FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/orders.fxml")); loader.setControllerFactory(type -> new OrdersController(orderRepository)); Parent root = loader.load();
Keep controllers thin. A controller should translate UI events into calls on a service or model, and translate model state back into widget state. Business logic does not belong here. A controller that is hard to unit-test is usually a controller doing too much.

Event Handlers: @FXML Methods vs. Lambda Registration

The onAction="#methodName" FXML syntax and calling button.setOnAction(e -> ...) in code are equivalent in result. Use FXML event references for handlers that are a natural part of the declared layout. Use lambda registration in initialize() when you need local variable capture or when the handler is being attached dynamically.

@FXML private void initialize() { // Dynamic handler that captures a loop variable — must be done in code for (Button btn : colorButtons) { final String color = btn.getId(); btn.setOnAction(e -> applyColor(color)); } }

Nested Controllers with fx:include

Large UIs are often split into reusable FXML fragments loaded with fx:include. Each included FXML can have its own controller. The parent controller can access the child controller by declaring an @FXML field named <fx:id of the include>Controller:

<!-- parent.fxml --> <BorderPane fx:controller="com.example.MainController" ...> <top> <fx:include fx:id="toolbar" source="toolbar.fxml"/> </top> </BorderPane>
// MainController.java @FXML private ToolbarController toolbarController; // injected automatically
Do not reference @FXML fields from the constructor. They are null at construction time. Any code that touches a widget must live in initialize() or an event handler — never in the constructor or a static initialiser.

Summary

The @FXML annotation is the glue between your declarative FXML file and your imperative Java controller. Remember the three key points: declare fx:controller in the FXML root element; annotate matching fields with @FXML (they can be private); and put all start-up widget logic in initialize(), not the constructor. In the next lesson you will use Scene Builder to generate and maintain FXML files visually, and see how it keeps the fx:id and fx:controller attributes in sync automatically.