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.

What You'll Learn:
  • 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.

Benefits of Single Window Architecture:
  • 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();
    }
}
How View Caching Works:
  1. When ApplicationWindowController is initialized, it creates all view instances
  2. Each view's createViewInstance() method loads the FXML and returns the root region
  3. The view instances are stored in private fields (the cache)
  4. 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);
}
Display Method Pattern:
  1. Auto-save: Check if coming from admin panel, if so, save data
  2. Clear container: Remove the current view
  3. Re-initialize (if needed): Some views need fresh data when displayed
  4. 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" />
Navigation Pattern:
  • 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);
    }
}
Global State Fields in Jukebox.java:
// 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);
    }
}
When to Re-initialize:
  • 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 ...
}
Benefits of Auto-Save:
  • 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
How Auto-Save Works:
  1. Each display method checks if the current view is the admin panel
  2. If it is, database.saveData() is called
  3. Then the container is cleared and the new view is added
  4. 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:

    1. Run the Jukebox application and navigate through all the views
    2. Watch what happens when you leave the Admin Panel - your changes are automatically saved
    3. Try clicking GO HOME from each view to see it always returns to home
    4. Look at ApplicationWindowController.java and find all the display methods
    5. Look at application-window.fxml and find the GO HOME button

    Common Pitfalls

    Pitfall 1: Memory Leaks with Caching

    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.

    Pitfall 2: Stale Data in Cached Views

    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.

    Pitfall 3: Direct Controller References

    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.

    Pitfall 4: Forgetting Auto-Save

    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

    Related Cookbook Sections