Navigation Between Views
The Jukebox project uses a single-window architecture where different views (screens) are displayed in a central container. This cookbook section explains how navigation between views works, using ApplicationWindowController as the primary example.
- Single window vs multiple window architecture
- Container-based view switching
- View caching for performance
- Navigation methods and flow
- Passing data between views
Single Window Architecture
Unlike applications that create a new window for each screen, Jukebox uses a single window architecture. This means there's only one Stage (window), and different views are shown inside it by adding and removing them from a container.
- Consistent User Experience: Users don't have to deal with multiple windows
- Simpler Management: Only one window to track, minimize, maximize, close
- Better Performance: Views can be cached and reused
- Easier Navigation: Switching between views is straightforward
- Cleaner Code: Less window management code
Implementation in application-window.fxml:
<VBox alignment="TOP_CENTER" ...>
<children>
<HBox>...</HBox> // Header with title and GO HOME button
<VBox fx:id="container" prefHeight="200" prefWidth="100" VBox.vgrow="ALWAYS" />
</children>
</VBox>
The container VBox is where all views are displayed. Its fx:id is "container", which matches a field in ApplicationWindowController:
public class ApplicationWindowController {
public VBox container;
// ...
}
View Caching Pattern
One of the key features of Jukebox's navigation system is that views are cached - they're created once and reused, rather than being recreated every time they're displayed. This improves performance, especially for complex views.
In ApplicationWindowController.java:
public class ApplicationWindowController {
// VIEW INSTANCES (cached for performance)
private Region chooseAlbumView;
private Region homeScreenView;
private Region playAlbumView;
private Region adminPanelView;
// Initialization - called automatically by FXMLLoader
public void initialize() {
// Create and cache all view instances
chooseAlbumView = ChooseAlbumController.createViewInstance();
homeScreenView = HomeScreenController.createViewInstance();
playAlbumView = PlayAlbumController.createViewInstance();
adminPanelView = AdminPanelController.createViewInstance();
// Display the home view as the initial screen
displayHomeView();
}
}
- When
ApplicationWindowControlleris initialized, it creates all view instances - Each view's
createViewInstance()method loads the FXML and returns the root region - The view instances are stored in private fields (the cache)
- When navigating, the cached view is reused rather than recreating it
Display Methods
For each view, there's a corresponding display method in ApplicationWindowController. These methods handle showing the specific view in the container.
Example: displayHomeView()
public void displayHomeView() {
// Save database changes if we're coming from the admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
// Remove any existing view from the container
container.getChildren().clear();
// Add the home screen view to the container
container.getChildren().add(homeScreenView);
}
Example: displayChooseAlbumView()
public void displayChooseAlbumView() {
// Save database changes if we're coming from the admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
// Remove any existing view from the container
container.getChildren().clear();
// Add the choose album view to the container
container.getChildren().add(chooseAlbumView);
}
Example: displayPlayAlbumView()
public void displayPlayAlbumView() {
// Save database changes if we're coming from the admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
// Re-initialize the play album controller to load the current selected album
Jukebox.playAlbumController.initialize();
// Remove any existing view from the container
container.getChildren().clear();
// Add the play album view to the container
container.getChildren().add(playAlbumView);
}
- Auto-save: Check if coming from admin panel, if so, save data
- Clear container: Remove the current view
- Re-initialize (if needed): Some views need fresh data when displayed
- Add new view: Add the target view to the container
Navigation Triggers
Navigation between views is triggered by user actions, typically button clicks. Let's look at how different controllers initiate navigation:
1. From Home Screen
In HomeScreenController.java:
public void onPlayModeClicked(ActionEvent actionEvent) {
Jukebox.applicationWindowController.displayChooseAlbumView();
}
public void onAdminModeClicked(ActionEvent actionEvent) {
Jukebox.applicationWindowController.displayAdminPanelView();
}
2. From Choose Album View
In ChooseAlbumController.java:
public void onPlayAlbumButtonClicked(ActionEvent actionEvent) {
Album selected = albumsTable.getSelectionModel().getSelectedItem();
if (selected != null) {
// Set the selected album globally
Jukebox.selectedAlbum = selected;
// Navigate to play album view
Jukebox.applicationWindowController.displayPlayAlbumView();
}
}
3. Global Navigation: GO HOME Button
In ApplicationWindowController.java:
public void onHomeButtonClicked(ActionEvent actionEvent) {
displayHomeView();
}
This is connected in application-window.fxml:
<Button mnemonicParsing="false" onAction="#onHomeButtonClicked" text="GO HOME" />
- Each view's controller has access to
Jukebox.applicationWindowController - To navigate, controllers call display methods on the ApplicationWindowController
- Data is passed between views using static fields in
Jukebox.java - The GO HOME button is always available, providing consistent navigation
Passing Data Between Views
Since Jukebox uses a single window architecture, data between views is shared through static fields in the Jukebox class. This allows different views to access the same data.
Example: Passing Selected Album
When a user selects an album in the Choose Album view and clicks Play, the selection is passed to the Play Album view:
// In ChooseAlbumController.java
public void onPlayAlbumButtonClicked(ActionEvent actionEvent) {
Album selected = albumsTable.getSelectionModel().getSelectedItem();
if (selected != null) {
// Store the selected album in a static field
Jukebox.selectedAlbum = selected;
// Navigate to play album view
Jukebox.applicationWindowController.displayPlayAlbumView();
}
}
// In PlayAlbumController.java
public void initialize() {
// Read the selected album from the static field
if (Jukebox.selectedAlbum != null) {
albumTitleLabel.setText(Jukebox.selectedAlbum.name);
albumArtistLabel.setText(Jukebox.selectedAlbum.artist);
// Display the songs
songsList.getItems().setAll(Jukebox.selectedAlbum.songs);
}
}
// These static fields hold application state between views
public static Database database; // All application data
public static Album selectedAlbum; // Currently selected album
public static ApplicationWindowController applicationWindowController;
public static ChooseAlbumController chooseAlbumController;
public static PlayAlbumController playAlbumController;
public static HomeScreenController homeScreenController;
public static AdminPanelController adminPanelController;
Re-initialization on Display
Some views need to refresh their data when they're displayed. For example, the Play Album view needs to show the currently selected album's songs. This is handled by re-initializing the controller before displaying:
// In ApplicationWindowController.java
public void displayPlayAlbumView() {
// ... save check and clear container ...
// Re-initialize the controller to ensure it has fresh data
Jukebox.playAlbumController.initialize();
// ... add view to container ...
}
In PlayAlbumController.java:
public void initialize() {
// This is called both when the view is first created
// AND when displayPlayAlbumView() is called
if (Jukebox.selectedAlbum != null) {
albumTitleLabel.setText(Jukebox.selectedAlbum.name);
albumArtistLabel.setText(Jukebox.selectedAlbum.artist);
songsList.getItems().setAll(Jukebox.selectedAlbum.songs);
}
}
- Always: Views that depend on external state (like selected album)
- When data changes: Views that display data that might have changed
- Never: Views that always display the same static content
Auto-Save on Navigation
One of the smart features of Jukebox's navigation system is auto-save. When leaving the Admin Panel, any changes are automatically saved to the database:
// In each display method of ApplicationWindowController.java
public void displaySomeView() {
// Check if we're coming from the admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData();
}
// ... rest of the method ...
}
- Prevents Data Loss: Users don't have to remember to click Save
- Transparent: Happens automatically in the background
- Conditional: Only saves when actually leaving the admin panel
- Consistent: Applied to all navigation paths from admin
- Each display method checks if the current view is the admin panel
- If it is,
database.saveData()is called - Then the container is cleared and the new view is added
- This ensures changes are saved whenever the user navigates away from admin
Complete Navigation Flow Example
Let's trace through a complete navigation flow from Home to Admin to Choose Album:
Step 1: User clicks Admin Mode on Home Screen
HomeScreenController.onAdminModeClicked() is called
Jukebox.applicationWindowController.displayAdminPanelView();
Step 2: displayAdminPanelView() is called
In ApplicationWindowController:
public void displayAdminPanelView() {
container.getChildren().clear();
container.getChildren().add(adminPanelView);
}
Admin Panel view is now displayed. User can add, edit, delete albums and songs.
Step 3: User clicks GO HOME
ApplicationWindowController.onHomeButtonClicked() is called
public void onHomeButtonClicked(ActionEvent actionEvent) {
displayHomeView();
}
Step 4: displayHomeView() is called
In ApplicationWindowController:
public void displayHomeView() {
// Check if coming from admin panel
if (container.getChildren().contains(adminPanelView)) {
Jukebox.database.saveData(); // Auto-save!
}
container.getChildren().clear();
container.getChildren().add(homeScreenView);
}
Admin panel changes are saved, home view is displayed.
Creating Your Own Navigation System
To implement a similar navigation system in your own application:
Step 1: Create a container in your main FXML
<VBox>
<!-- Header, menu, etc. -->
<VBox fx:id="container" VBox.vgrow="ALWAYS" />
</VBox>
Step 2: Create display methods in your main controller
private Region view1;
private Region view2;
public void initialize() {
view1 = View1Controller.createViewInstance();
view2 = View2Controller.createViewInstance();
displayView1();
}
public void displayView1() {
container.getChildren().clear();
container.getChildren().add(view1);
}
public void displayView2() {
container.getChildren().clear();
container.getChildren().add(view2);
}
Step 3: Add navigation buttons
<Button onAction="#displayView1" text="View 1" />
<Button onAction="#displayView2" text="View 2" />
Try This:
- Run the Jukebox application and navigate through all the views
- Watch what happens when you leave the Admin Panel - your changes are automatically saved
- Try clicking GO HOME from each view to see it always returns to home
- Look at
ApplicationWindowController.javaand find all the display methods - Look at
application-window.fxmland find the GO HOME button
Common Pitfalls
While caching improves performance, if your views hold references to large objects or have listeners that aren't cleaned up, you might have memory leaks. Make sure to clean up resources when views are no longer needed.
When reusing cached views, make sure to refresh their data. That's why PlayAlbumController.initialize() is called every time the view is displayed - to ensure it shows current data.
In Jukebox, controllers access each other through static fields. This is fine for a learning project, but in larger applications, this creates tight coupling. Consider using an event bus or dependency injection instead.
If you add a new navigation path from the admin panel, don't forget to add the auto-save check. Otherwise, users might lose their changes.
Key Takeaways
- Jukebox uses a single window with container-based view switching
- Views are cached for better performance
ApplicationWindowControllermanages all navigation- Each view has a corresponding display method
- Navigation is triggered by button clicks in view controllers
- Data is passed between views using static fields in
Jukebox.java - Auto-save happens when leaving the Admin Panel
- GO HOME button provides consistent navigation to home screen