Controllers & @FXML
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:
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).
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:
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.
Passing Data Between Controllers
There is no built-in "router" in JavaFX. Common patterns for transferring data when navigating between screens:
- Setter injection (shown above) — the calling code calls a public setter on the next controller after
load()but beforeshow(). - Shared model / service singleton — both controllers hold a reference to the same service or observable model object; the second controller reads it in
initialize(). - Constructor injection via a controller factory — pass a lambda as the second argument to the
FXMLLoaderconstructor to construct controllers yourself, enabling proper dependency injection.
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.
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:
@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.