App structure and navigation
IA tooling, not an exam topic. These are JavaFX patterns to adapt for your IA. Nothing here is examinable, and your IA must be your own work.
This page is about the shape of a whole app: when setup code runs, menus, quitting cleanly, opening a second window, passing data between screens, sharing data across the app, splitting into tabs, and a coat of CSS to make it look finished.
New here? Read Start here: how every recipe fits together first; it explains the two places you edit and the fx:id and onAction links every recipe relies on.
Run setup code when a screen loads
What this does: uses the initialize() method, which JavaFX calls automatically once a screen’s FXML has loaded, to do any opening setup.
When you would use it in your IA: filling a table or combo box with starting data, setting a default selection, loading saved records, or focusing the first field. Anything that should be ready before the user sees the screen.
In SceneBuilder (the layout): add whatever controls the setup touches and give them fx:ids (here a Label, fx:id lblWelcome). No handler is needed; initialize() runs on its own.
<Label fx:id="lblWelcome" />
In the controller (the .java file):
import javafx.fxml.FXML;
import javafx.scene.control.Label;
@FXML private Label lblWelcome;
@FXML
public void initialize() {
// Runs once, automatically, after the controls are built and injected.
// your IA logic here: your opening setup
lblWelcome.setText("Welcome back");
loadSavedData();
}
Where your own logic goes: the body of initialize(), marked // your IA logic here. That is your column setup, data loading, and defaults. The method’s timing is wiring JavaFX gives you.
Make it your own:
- Put your column setup, data loading, and default selections here.
- The method must be named
initializefor JavaFX to call it. The@FXMLannotation lets it be private.
Watch out for:
- Any
fx:idyou use ininitialize()must match a control in the layout exactly, or that field isnull. - The
@FXMLfields arenulluntil just beforeinitialize()runs, so do not try to use the controls in the controller’s constructor.initialize()is the earliest safe place. - It runs once per screen load, not every time the window is shown again.
Mix with: Put the cursor where the user should start, Display rows in a scrollable table.
Add actions to the menu bar
What this does: puts a menu bar at the top of the window with items that run your methods when clicked.
When you would use it in your IA: the standard File, Edit, Help menus, or any grouped set of commands you do not want as on-screen buttons.
In SceneBuilder (the layout): add a MenuBar, add Menus and MenuItems inside it, and set each MenuItem’s onAction to a handler name.
<MenuBar>
<menus>
<Menu text="File">
<items>
<MenuItem text="Save" onAction="#handleSave" />
<MenuItem text="Exit" onAction="#handleExit" />
</items>
</Menu>
<Menu text="Help">
<items>
<MenuItem text="About" onAction="#handleAbout" />
</items>
</Menu>
</menus>
</MenuBar>
In the controller (the .java file):
@FXML
private void handleSave() {
// your IA logic here: save the data
}
@FXML
private void handleExit() {
// your IA logic here: quit the app
}
@FXML
private void handleAbout() {
// your IA logic here: show an information dialog
}
Where your own logic goes: the body of each handler, marked // your IA logic here. The menu structure and the wiring to the handlers are reusable.
Make it your own:
- Rename the menus and items, and point each
onActionat a method that exists in your controller. - A
MenuItem’s handler works exactly like a button’s, so you can reuse the same method for a menu item and a toolbar button.
Watch out for:
- Each
onActionname must match an@FXMLmethod exactly, including the#. A typo gives a load-time error.
Mix with: Quit the app cleanly, Tell the user something.
Quit the app cleanly
What this does: closes the application. There are two ways, and the difference matters when you have cleanup to do.
When you would use it in your IA: an Exit menu item or a Quit button.
In SceneBuilder (the layout): add the Exit menu item or Quit button and set its onAction to handleExit.
<Button text="Quit" onAction="#handleExit" />
In the controller (the .java file):
import javafx.application.Platform;
@FXML
private void handleExit() {
// your IA logic here: save anything unsaved before leaving
quit();
}
// Cleaner: shuts JavaFX down, running your Application.stop() first.
private void quit() {
Platform.exit();
}
// Harder stop: ends the program immediately.
private void forceQuit() {
System.exit(0);
}
Where your own logic goes: any save-before-quit step, marked // your IA logic here. The two ways to exit are reusable wiring.
Make it your own:
- Prefer
Platform.exit()for a normal quit. If yourApplicationclass overridesstop(), that is where you save data or release resources, andPlatform.exit()makes sure it runs. - Use
System.exit(0)only when you genuinely need an immediate stop without that cleanup.
Watch out for:
System.exit(0)does not runApplication.stop(), so any save-on-exit code there is skipped. The0means “ended normally”.- Closing the last window can also end the app, depending on your setup, so test which path your Exit control actually takes.
Mix with: Add actions to the menu bar, Ask before doing something destructive.
Open a second window
What this does: loads another FXML screen into a new Stage and shows it as a separate window.
When you would use it in your IA: an Add or Edit dialog, a details window, or a settings window that sits on top of the main screen.
In SceneBuilder (the layout): you need two FXML files: the main screen with a button that opens the second (onAction handleOpenDetails), and the second screen (Details.fxml) with its own controller.
<Button text="Details..." onAction="#handleOpenDetails" />
In the controller (the .java file):
Imports:
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
Method:
@FXML
private void handleOpenDetails() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("Details.fxml"));
Parent root = loader.load();
Stage stage = new Stage();
stage.setTitle("Details");
stage.setScene(new Scene(root));
stage.show();
} catch (IOException e) {
// handle the error
}
}
Where your own logic goes: this recipe is mostly wiring. Your part is which second screen to open (Details.fxml) and, if it needs data, the next recipe.
Make it your own:
- Change
"Details.fxml"to your second screen’s FXML file, and the title to suit. - Keep the example to two windows. More than that is usually better handled with tabs or by swapping scenes.
Watch out for:
- The method named in
onAction(handleOpenDetails) must exist in the controller, or the screen will not load. getResource("Details.fxml")looks for the file next to your class. If it returnsnull, the path or file name is wrong.- A new
Stageopens alongside the first window. If you want the second window to block the first until it is closed, callstage.initModality(Modality.APPLICATION_MODAL)before showing it.
Mix with: Pass information between screens, Share data across the whole app.
Pass information between screens
What this does: hands data to a second screen by getting that screen’s controller from the FXMLLoader and calling a public setter on it.
When you would use it in your IA: opening an edit window for the row the user selected, or sending a chosen value into a details screen.
In SceneBuilder (the layout): the same two FXML files as the previous recipe. The second screen’s controller (EditController) gets a public setter.
In the controller (the .java file): this builds on the previous recipe. The new lines are getting the controller and calling its setter.
private void editItem(Item selected) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("Edit.fxml"));
Parent root = loader.load();
// Get the controller of the screen we just loaded, then pass data in.
EditController controller = loader.getController();
controller.setItem(selected);
Stage stage = new Stage();
stage.setScene(new Scene(root));
stage.show();
} catch (IOException e) {
// handle the error
}
}
The second screen’s controller has a public setter:
public class EditController {
private Item item;
public void setItem(Item item) {
this.item = item;
// your IA logic here: fill this screen's fields from the item
}
}
Where your own logic goes: the body of setItem(...), marked // your IA logic here, where you fill the second screen from the data passed in. Getting the controller and calling the setter is reusable wiring.
Make it your own:
- Replace
EditControllerandsetItem(...)with your own controller and a setter that takes whatever data the screen needs. - The setter is also the natural place to fill in the second screen’s fields from the data you passed in.
Watch out for:
- Call
loader.getController()afterloader.load(), not before. Before the load, there is no controller yet and it returnsnull. - Pass data through a public setter, not by reaching into the other controller’s fields directly. The setter keeps the screens loosely connected.
Mix with: Open a second window, Find out which row the user clicked.
Share data across the whole app
What this does: keeps one shared object that every screen can read from and write to, using a small Singleton class so there is only ever one of it.
When you would use it in your IA: the logged-in user, app-wide settings, or a single shopping cart that several screens all work with.
In SceneBuilder (the layout): nothing to add; this is a plain class with no screen of its own.
In the controller (the .java file):
public class AppSettings {
private static AppSettings instance; // the one and only instance
// your IA logic here: the fields you want to share app-wide
private String currentUser;
private AppSettings() {
// private constructor: nothing outside can make a second one
}
public static AppSettings getInstance() {
if (instance == null) {
instance = new AppSettings();
}
return instance;
}
public String getCurrentUser() {
return currentUser;
}
public void setCurrentUser(String currentUser) {
this.currentUser = currentUser;
}
}
Any screen uses the same shared object:
AppSettings.getInstance().setCurrentUser("Sam"); // on the login screen
String user = AppSettings.getInstance().getCurrentUser(); // on any other screen
Where your own logic goes: the fields and getters/setters you add for your shared data, marked // your IA logic here. The Singleton structure (private constructor, static getInstance()) is reusable wiring.
Make it your own:
- Add fields and getters and setters for whatever you need to share across screens.
- The two parts that make it a Singleton are the
privateconstructor and thestatic getInstance()method. Keep both.
Watch out for:
- A Singleton is shared global state. It is handy for genuinely app-wide data, but do not put everything in it, or screens become hard to follow. Most data should still travel through the setter approach above.
- This is the same Singleton idea from the HL design-patterns material. SL students can use it as a recipe without needing the full theory.
Mix with: Pass information between screens, Save and load text.
Split a big app into tabs
What this does: puts several sections in one window as tabbed pages, so the user switches between them without opening new windows.
When you would use it in your IA: separating, say, a data-entry section, a list view, and a settings section in a single window.
In SceneBuilder (the layout): add a TabPane, add a Tab per section, and put that section’s controls inside each Tab’s content.
<TabPane>
<tabs>
<Tab text="Items" closable="false">
<content>
<!-- the controls for this section go here -->
</content>
</Tab>
<Tab text="Settings" closable="false">
<content>
<!-- the controls for this section go here -->
</content>
</Tab>
</tabs>
</TabPane>
In the controller (the .java file): nothing special is required; the controls inside each tab are wired with fx:id and onAction exactly as they would be on a single screen.
Where your own logic goes: the controls inside each tab and their handlers, which are ordinary recipes from the rest of this cookbook. The TabPane is layout wiring.
Make it your own:
- Add one
Tabper section and put that section’s controls inside its<content>. - Set
closable="false"on tabs the user should not be able to close.
Watch out for:
- All the tabs share one window and, by default, one controller. For a large app, you can load a separate FXML into each tab’s content so each section has its own controller.
- Tabs suit sections of one task. For genuinely separate jobs, a second window is often clearer.
Mix with: Open a second window, Make your app look finished.
Make your app look finished
What this does: attaches a CSS stylesheet to the scene so you can improve spacing and colour without changing your Java code.
When you would use it in your IA: the cheap, high-impact step near the end of a build, to move the app from “default grey” to something that looks considered.
In SceneBuilder (the layout): give the nodes you want to target a style class (in the Properties panel, “Style Class”), for example primary-heading on a title label. You can also attach the stylesheet to the root here, or do it in code as shown.
In the controller (the .java file): attach the stylesheet when you build the scene.
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
A small style.css next to your classes:
.root {
-fx-font-family: "Segoe UI", sans-serif;
-fx-background-color: #f4f6fb;
}
.button {
-fx-background-color: #2d6cdf;
-fx-text-fill: white;
-fx-padding: 8 16 8 16;
-fx-background-radius: 6;
}
.primary-heading {
-fx-font-size: 18px;
-fx-font-weight: bold;
}
Give a node a style class in code so the matching rule applies:
lblTitle.getStyleClass().add("primary-heading");
Where your own logic goes: the colours, fonts, and spacing in the stylesheet are your design choices. Attaching the stylesheet is reusable wiring.
Make it your own:
- Change the colours, fonts, and spacing to suit your app. Keep the palette small, two or three colours is plenty.
- Style whole control types (
.button) for a consistent look, and use named style classes (.primary-heading) for specific nodes.
Watch out for:
- JavaFX CSS is similar to web CSS but not identical: the property names start with
-fx-(for example-fx-background-color, notbackground-color). Web CSS property names will be ignored. - If the stylesheet seems to have no effect, check that
getResource("style.css")is finding the file. Anullthere means the path is wrong.
Mix with: Split a big app into tabs, Put an image on a button.