Widgets

Navigation Hub

Introduction

Navigation Hubs are a central place where you can manage the navigation for all your widgets. Out the box you can create bottom, top and journey navigation layouts in seconds.

Let's imagine you have an app and you want to add a bottom navigation bar and allow users to navigate between different tabs in your app.

You can use a Navigation Hub to build this.

Let's dive into how you can use a Navigation Hub in your app.

Basic Usage

You can create a Navigation Hub using the below command.

metro make:navigation_hub base

The command will walk you through an interactive setup:

  1. Choose a layout type - Select between navigation_tabs (bottom navigation) or journey_states (sequential flow).
  2. Enter tab/state names - Provide comma-separated names for your tabs or journey states.

This will create files under your resources/pages/navigation_hubs/base/ directory:

  • base_navigation_hub.dart - The main hub widget
  • tabs/ or states/ - Contains the child widgets for each tab or journey state

Here's what a generated Navigation Hub looks like:

import 'package:flutter/material.dart';
import 'package:nylo_framework/nylo_framework.dart';
import '/resources/pages/navigation_hubs/base/tabs/home_tab_widget.dart';
import '/resources/pages/navigation_hubs/base/tabs/settings_tab_widget.dart';

class BaseNavigationHub extends NyStatefulWidget with BottomNavPageControls {
  static RouteView path = ("/base", (_) => BaseNavigationHub());

  BaseNavigationHub()
      : super(
            child: () => _BaseNavigationHubState(),
            stateName: path.stateName());

  /// State actions
  static NavigationHubStateActions stateActions = NavigationHubStateActions(path.stateName());
}

class _BaseNavigationHubState extends NavigationHub<BaseNavigationHub> {

  /// Layout builder
  @override
  NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();

  /// Should the state be maintained
  @override
  bool get maintainState => true;

  /// The initial index
  @override
  int get initialIndex => 0;

  /// Navigation pages
  _BaseNavigationHubState() : super(() => {
      0: NavigationTab.tab(title: "Home", page: HomeTab()),
      1: NavigationTab.tab(title: "Settings", page: SettingsTab()),
  });

  /// Handle the tap event
  @override
  onTap(int index) {
    super.onTap(index);
  }
}

You can see that the Navigation Hub has two tabs, Home and Settings.

The layout method returns the layout type for the hub. It receives a BuildContext so you can access theme data and media queries when configuring your layout.

You can create more tabs by adding NavigationTab's to the Navigation Hub.

First, you need to create a new widget using Metro.

metro make:stateful_widget news_tab

You can also create multiple widgets at once.

metro make:stateful_widget news_tab,notifications_tab

Then, you can add the new widget to the Navigation Hub.

_BaseNavigationHubState() : super(() => {
    0: NavigationTab.tab(title: "Home", page: HomeTab()),
    1: NavigationTab.tab(title: "Settings", page: SettingsTab()),
    2: NavigationTab.tab(title: "News", page: NewsTab()),
});

To use the Navigation Hub, add it to your router as the initial route:

import 'package:nylo_framework/nylo_framework.dart';

appRouter() => nyRoutes((router) {
    ...
    router.add(BaseNavigationHub.path).initialRoute();
});

// or navigate to the Navigation Hub from anywhere in your app

routeTo(BaseNavigationHub.path);

There's a lot more you can do with a Navigation Hub, let's dive into some of the features.

Bottom Navigation

You can set the layout to a bottom navigation bar by returning NavigationHubLayout.bottomNav from the layout method.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();

You can customize the bottom navigation bar by setting properties like the following:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
        backgroundColor: Colors.white,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        elevation: 8.0,
        iconSize: 24.0,
        selectedFontSize: 14.0,
        unselectedFontSize: 12.0,
        showSelectedLabels: true,
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
    );

You can apply a preset style to your bottom navigation bar using the style parameter.

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
    style: BottomNavStyle.material(), // Default Flutter material style
);

Custom Nav Bar Builder

For complete control over your navigation bar, you can use the navBarBuilder parameter.

This allows you to build any custom widget while still receiving the navigation data.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
        navBarBuilder: (context, data) {
            return MyCustomNavBar(
                items: data.items,
                currentIndex: data.currentIndex,
                onTap: data.onTap,
            );
        },
    );

The NavBarData object contains:

Property Type Description
items List<BottomNavigationBarItem> The navigation bar items
currentIndex int The currently selected index
onTap ValueChanged<int> Callback when a tab is tapped

Here's an example of a fully custom glass nav bar:

NavigationHubLayout.bottomNav(
    navBarBuilder: (context, data) {
        return Padding(
            padding: EdgeInsets.all(16),
            child: ClipRRect(
                borderRadius: BorderRadius.circular(25),
                child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
                    child: Container(
                        decoration: BoxDecoration(
                            color: Colors.white.withValues(alpha: 0.7),
                            borderRadius: BorderRadius.circular(25),
                        ),
                        child: BottomNavigationBar(
                            items: data.items,
                            currentIndex: data.currentIndex,
                            onTap: data.onTap,
                            backgroundColor: Colors.transparent,
                            elevation: 0,
                        ),
                    ),
                ),
            ),
        );
    },
)

Note: When using navBarBuilder, the style parameter is ignored.

Top Navigation

You can change the layout to a top navigation bar by returning NavigationHubLayout.topNav from the layout method.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.topNav();

You can customize the top navigation bar by setting properties like the following:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.topNav(
        backgroundColor: Colors.white,
        labelColor: Colors.blue,
        unselectedLabelColor: Colors.grey,
        indicatorColor: Colors.blue,
        indicatorWeight: 3.0,
        isScrollable: false,
        hideAppBarTitle: true,
    );

Journey Navigation

You can change the layout to a journey navigation by returning NavigationHubLayout.journey from the layout method.

This is great for onboarding flows or multi-step forms.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
        progressStyle: JourneyProgressStyle(
          indicator: JourneyProgressIndicator.segments(),
        ),
    );

You can also set a backgroundGradient for the journey layout:

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    backgroundGradient: LinearGradient(
        colors: [Colors.blue, Colors.purple],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
    ),
    progressStyle: JourneyProgressStyle(
      indicator: JourneyProgressIndicator.linear(),
    ),
);

Note: When backgroundGradient is set, it takes precedence over backgroundColor.

If you want to use the journey navigation layout, your widgets should use JourneyState as it contains a lot of helper methods to help you manage the journey.

You can create the whole journey using the make:navigation_hub command with the journey_states layout:

metro make:navigation_hub onboarding
# Select: journey_states
# Enter: welcome, personal_info, add_photos

This will create the hub and all journey state widgets under resources/pages/navigation_hubs/onboarding/states/.

Or you can create individual journey widgets using:

metro make:journey_widget welcome,phone_number_step,add_photos_step

You can then add the new widgets to the Navigation Hub.

_MyNavigationHubState() : super(() => {
    0: NavigationTab.journey(
        page: Welcome(),
    ),
    1: NavigationTab.journey(
        page: PhoneNumberStep(),
    ),
    2: NavigationTab.journey(
        page: AddPhotosStep(),
    ),
});

Journey Progress Styles

You can customize the progress indicator style by using the JourneyProgressStyle class.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
        progressStyle: JourneyProgressStyle(
            indicator: JourneyProgressIndicator.linear(
                activeColor: Colors.blue,
                inactiveColor: Colors.grey,
                thickness: 4.0,
            ),
        ),
    );

You can use the following progress indicators:

  • JourneyProgressIndicator.none(): Renders nothing - useful for hiding the indicator on a specific tab.
  • JourneyProgressIndicator.linear(): Linear progress bar.
  • JourneyProgressIndicator.dots(): Dots-based progress indicator.
  • JourneyProgressIndicator.numbered(): Numbered step progress indicator.
  • JourneyProgressIndicator.segments(): Segmented progress bar style.
  • JourneyProgressIndicator.circular(): Circular progress indicator.
  • JourneyProgressIndicator.timeline(): Timeline-style progress indicator.
  • JourneyProgressIndicator.custom(): Custom progress indicator using a builder function.
@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    progressStyle: JourneyProgressStyle(
        indicator: JourneyProgressIndicator.custom(
            builder: (context, currentStep, totalSteps, percentage) {
                return LinearProgressIndicator(
                    value: percentage,
                    backgroundColor: Colors.grey[200],
                    color: Colors.blue,
                    minHeight: 4.0,
                );
            },
        ),
    ),
);

You can customize the progress indicator position and padding within the JourneyProgressStyle:

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    progressStyle: JourneyProgressStyle(
        indicator: JourneyProgressIndicator.dots(),
        position: ProgressIndicatorPosition.bottom,
        padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
    ),
);

You can use the following progress indicator positions:

  • ProgressIndicatorPosition.top: Progress indicator at the top of the screen.
  • ProgressIndicatorPosition.bottom: Progress indicator at the bottom of the screen.

Per-Tab Progress Style Override

You can override the layout-level progressStyle on individual tabs using NavigationTab.journey(progressStyle: ...). Tabs without their own progressStyle inherit the layout default. Tabs with no layout default and no per-tab style will not show a progress indicator.

_MyNavigationHubState() : super(() => {
    0: NavigationTab.journey(
        page: Welcome(),
    ),
    1: NavigationTab.journey(
        page: PhoneNumberStep(),
        progressStyle: JourneyProgressStyle(
            indicator: JourneyProgressIndicator.numbered(),
        ), // overrides the layout default for this tab only
    ),
    2: NavigationTab.journey(
        page: AddPhotosStep(),
    ),
});

JourneyState

The JourneyState class extends NyState with journey-specific functionality to make it easier to create onboarding flows and multi-step journeys.

To create a new JourneyState, you can use the below command.

metro make:journey_widget onboard_user_dob

Or if you want to create multiple widgets at once, you can use the following command.

metro make:journey_widget welcome,phone_number_step,add_photos_step

Here's what a generated JourneyState widget looks like:

import 'package:flutter/material.dart';
import '/resources/pages/navigation_hubs/onboarding/onboarding_navigation_hub.dart';
import '/resources/widgets/buttons/buttons.dart';
import 'package:nylo_framework/nylo_framework.dart';

class Welcome extends StatefulWidget {
  const Welcome({super.key});

  @override
  createState() => _WelcomeState();
}

class _WelcomeState extends JourneyState<Welcome> {
  _WelcomeState() : super(
      navigationHubState: OnboardingNavigationHub.path.stateName());

  @override
  get init => () {
    // Your initialization logic here
  };

  @override
  Widget view(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Expanded(
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
                  const SizedBox(height: 20),
                  Text('This onboarding journey will help you get started.'),
                ],
              ),
            ),
          ),

          // Navigation buttons
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              if (!isFirstStep)
                Flexible(
                  child: Button.textOnly(
                    text: "Back",
                    textColor: Colors.black87,
                    onPressed: onBackPressed,
                  ),
                )
              else
                const SizedBox.shrink(),
              Flexible(
                child: Button.primary(
                  text: "Continue",
                  onPressed: nextStep,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  /// Check if the journey can continue to the next step
  @override
  Future<bool> canContinue() async {
    return true;
  }

  /// Called before navigating to the next step
  @override
  Future<void> onBeforeNext() async {
    // E.g. save data to session
  }

  /// Called when the journey is complete (at the last step)
  @override
  Future<void> onComplete() async {}
}

You'll notice that the JourneyState class uses nextStep to navigate forward and onBackPressed to go back.

The nextStep method runs through the full validation lifecycle: canContinue() -> onBeforeNext() -> navigate (or onComplete() if at last step) -> onAfterNext().

You can also use buildJourneyContent to build a structured layout with optional navigation buttons:

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
          const SizedBox(height: 20),
          Text('This onboarding journey will help you get started.'),
        ],
      ),
      nextButton: Button.primary(
        text: isLastStep ? "Get Started" : "Continue",
        onPressed: nextStep,
      ),
      backButton: isFirstStep ? null : Button.textOnly(
        text: "Back",
        textColor: Colors.black87,
        onPressed: onBackPressed,
      ),
    );
}

Here are the properties you can use in the buildJourneyContent method.

Property Type Description
content Widget The main content of the page.
nextButton Widget? The next button widget.
backButton Widget? The back button widget.
contentPadding EdgeInsetsGeometry The padding for the content.
header Widget? The header widget.
footer Widget? The footer widget.
crossAxisAlignment CrossAxisAlignment The cross axis alignment of the content.

JourneyState Helper Methods

The JourneyState class has helper methods and properties that you can use to customize the behavior of your journey.

Method / Property Description
nextStep() Navigate to the next step with validation. Returns Future<bool>.
previousStep() Navigate to the previous step. Returns Future<bool>.
onBackPressed() Simple helper for navigating to the previous step.
onComplete() Called when the journey is complete (at the last step).
onBeforeNext() Called before navigating to the next step.
onAfterNext() Called after navigating to the next step.
canContinue() Validation check before navigating to the next step.
isFirstStep Returns true if this is the first step in the journey.
isLastStep Returns true if this is the last step in the journey.
currentStep Returns the current step index (0-based).
totalSteps Returns the total number of steps.
completionPercentage Returns the completion percentage (0.0 to 1.0).
goToStep(int index) Jump to a specific step by index.
goToNextStep() Jump to the next step (no validation).
goToPreviousStep() Jump to the previous step (no validation).
goToFirstStep() Jump to the first step.
goToLastStep() Jump to the last step.
exitJourney() Exit the journey by popping the root navigator.
resetCurrentStep() Reset the current step's state.
onJourneyComplete Callback when the journey completes (override in last step).
buildJourneyPage() Build a full-screen journey page with Scaffold.

nextStep

The nextStep method navigates to the next step with full validation. It runs through the lifecycle: canContinue() -> onBeforeNext() -> navigate or onComplete() -> onAfterNext().

You can pass force: true to bypass validation and directly navigate.

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
        content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
                ...
            ],
        ),
        nextButton: Button.primary(
            text: isLastStep ? "Get Started" : "Continue",
            onPressed: nextStep, // runs validation then navigates
        ),
    );
}

To skip validation:

onPressed: () => nextStep(force: true),

previousStep

The previousStep method navigates to the previous step. Returns true if successful, false if already at the first step.

onPressed: () async {
    bool success = await previousStep();
    if (!success) {
      // Already at first step
    }
},

onBackPressed

The onBackPressed method is a simple helper that calls previousStep() internally.

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
        content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
                ...
            ],
        ),
        backButton: isFirstStep ? null : Button.textOnly(
            text: "Back",
            textColor: Colors.black87,
            onPressed: onBackPressed,
        ),
    );
}

onComplete

The onComplete method is called when nextStep() is triggered on the last step (after validation passes).

@override
Future<void> onComplete() async {
    print("Journey completed");
}

onBeforeNext

The onBeforeNext method is called before navigating to the next step.

E.g. if you want to save data before navigating to the next step, you can do it here.

@override
Future<void> onBeforeNext() async {
    // E.g. save data to session
    // session('onboarding', {
    //   'name': 'Anthony Gordon',
    //   'occupation': 'Software Engineer',
    // });
}

onAfterNext

The onAfterNext method is called after navigating to the next step.

@override
Future<void> onAfterNext() async {
    // print('Navigated to the next step');
}

canContinue

The canContinue method is called when nextStep() is triggered. Return false to prevent navigation.

@override
Future<bool> canContinue() async {
    // Perform your validation logic here
    // Return true if the journey can continue, false otherwise
    if (nameController.text.isEmpty) {
        showToastSorry(description: "Please enter your name");
        return false;
    }
    return true;
}

isFirstStep

The isFirstStep property returns true if this is the first step in the journey.

backButton: isFirstStep ? null : Button.textOnly(
    text: "Back",
    textColor: Colors.black87,
    onPressed: onBackPressed,
),

isLastStep

The isLastStep property returns true if this is the last step in the journey.

nextButton: Button.primary(
    text: isLastStep ? "Get Started" : "Continue",
    onPressed: nextStep,
),

currentStep

The currentStep property returns the current step index (0-based).

Text("Step ${currentStep + 1} of $totalSteps"),

totalSteps

The totalSteps property returns the total number of steps in the journey.

completionPercentage

The completionPercentage property returns the completion percentage as a value from 0.0 to 1.0.

LinearProgressIndicator(value: completionPercentage),

goToStep

The goToStep method jumps directly to a specific step by index. This does not trigger validation.

nextButton: Button.primary(
    text: "Skip to photos",
    onPressed: () {
        goToStep(2); // jump to step index 2
    },
),

goToNextStep

The goToNextStep method jumps to the next step without validation. If already at the last step, it does nothing.

onPressed: () {
    goToNextStep(); // skip validation and go to next step
},

goToPreviousStep

The goToPreviousStep method jumps to the previous step without validation. If already at the first step, it does nothing.

onPressed: () {
    goToPreviousStep();
},

goToFirstStep

The goToFirstStep method jumps to the first step.

onPressed: () {
    goToFirstStep();
},

goToLastStep

The goToLastStep method jumps to the last step.

onPressed: () {
    goToLastStep();
},

exitJourney

The exitJourney method exits the journey by popping the root navigator.

onPressed: () {
    exitJourney(); // pop the root navigator
},

resetCurrentStep

The resetCurrentStep method resets the current step's state.

onPressed: () {
    resetCurrentStep();
},

onJourneyComplete

The onJourneyComplete getter can be overridden in the last step of your journey to define what happens when the user completes the flow.

class _CompleteStepState extends JourneyState<CompleteStep> {
  _CompleteStepState() : super(
      navigationHubState: OnboardingNavigationHub.path.stateName());

  /// Callback when journey completes
  @override
  void Function()? get onJourneyComplete => () {
    // Navigate to your home page or next destination
    routeTo(HomePage.path);
  };

  @override
  Widget view(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          ...
          Button.primary(
            text: "Get Started",
            onPressed: onJourneyComplete, // triggers the completion callback
          ),
        ],
      ),
    );
  }
}

buildJourneyPage

The buildJourneyPage method builds a full-screen journey page wrapped in a Scaffold with SafeArea.

@override
Widget view(BuildContext context) {
    return buildJourneyPage(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
        ],
      ),
      nextButton: Button.primary(
        text: "Continue",
        onPressed: nextStep,
      ),
      backgroundColor: Colors.white,
    );
}
Property Type Description
content Widget The main content of the page.
nextButton Widget? The next button widget.
backButton Widget? The back button widget.
contentPadding EdgeInsetsGeometry The padding for the content.
header Widget? The header widget.
footer Widget? The footer widget.
backgroundColor Color? The background color of the Scaffold.
appBar Widget? An optional AppBar widget.
crossAxisAlignment CrossAxisAlignment The cross axis alignment of the content.

Navigating to widgets within a tab

You can navigate to widgets within a tab by using the pushTo helper.

Inside your tab, you can use the pushTo helper to navigate to another widget.

_HomeTabState extends State<HomeTab> {
    ...
    void _navigateToSettings() {
        pushTo(SettingsPage());
    }
    ...
}

You can also pass data to the widget you're navigating to.

_HomeTabState extends State<HomeTab> {
    ...
    void _navigateToSettings() {
        pushTo(SettingsPage(), data: {"name": "Anthony"});
    }
    ...
}

Tabs

Tabs are the main building blocks of a Navigation Hub.

You can add tabs to a Navigation Hub using the NavigationTab class and its named constructors.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.tab(
            title: "Home",
            page: HomeTab(),
            icon: Icon(Icons.home),
            activeIcon: Icon(Icons.home),
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

In the above example, we've added two tabs to the Navigation Hub, Home and Settings.

You can use different kinds of tabs:

  • NavigationTab.tab() - A standard navigation tab.
  • NavigationTab.badge() - A tab with a badge count.
  • NavigationTab.alert() - A tab with an alert indicator.
  • NavigationTab.journey() - A tab for journey navigation layouts.

Adding Badges to Tabs

We've made it easy to add badges to your tabs.

Badges are a great way to show users that there's something new in a tab.

Example, if you have a chat app, you can show the number of unread messages in the chat tab.

To add a badge to a tab, you can use the NavigationTab.badge constructor.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.badge(
            title: "Chats",
            page: ChatTab(),
            icon: Icon(Icons.message),
            activeIcon: Icon(Icons.message),
            initialCount: 10,
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

In the above example, we've added a badge to the Chat tab with an initial count of 10.

You can also update the badge count programmatically.

/// Increment the badge count
BaseNavigationHub.stateActions.incrementBadgeCount(tab: 0);

/// Update the badge count
BaseNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 5);

/// Clear the badge count
BaseNavigationHub.stateActions.clearBadgeCount(tab: 0);

By default, the badge count will be remembered. If you want to clear the badge count each session, you can set rememberCount to false.

0: NavigationTab.badge(
    title: "Chats",
    page: ChatTab(),
    icon: Icon(Icons.message),
    activeIcon: Icon(Icons.message),
    initialCount: 10,
    rememberCount: false,
),

Adding Alerts to Tabs

You can add alerts to your tabs.

Sometimes you might not want to show a badge count, but you want to show an alert indicator to the user.

To add an alert to a tab, you can use the NavigationTab.alert constructor.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.alert(
            title: "Chats",
            page: ChatTab(),
            icon: Icon(Icons.message),
            activeIcon: Icon(Icons.message),
            alertColor: Colors.red,
            alertEnabled: true,
            rememberAlert: false,
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

This will add an alert to the Chat tab with a red color.

You can also update the alert programmatically.

/// Enable the alert
BaseNavigationHub.stateActions.alertEnableTab(tab: 0);

/// Disable the alert
BaseNavigationHub.stateActions.alertDisableTab(tab: 0);

Initial Index

By default, the Navigation Hub starts on the first tab (index 0). You can change this by overriding the initialIndex getter.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    int get initialIndex => 1; // Start on the second tab
    ...
}

Maintaining state

By default, the state of the Navigation Hub is maintained.

This means that when you navigate to a tab, the state of the tab is preserved.

If you want to clear the state of the tab each time you navigate to it, you can set maintainState to false.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    bool get maintainState => false;
    ...
}

onTap

You can override the onTap method to add custom logic when a tab is tapped.

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    onTap(int index) {
        // Add custom logic here
        // E.g. track analytics, show confirmation, etc.
        super.onTap(index); // Always call super to handle the tab switch
    }
}

State Actions

State actions are a way to interact with the Navigation Hub from anywhere in your app.

Here are the state actions you can use:

/// Reset the tab at a given index
/// E.g. MyNavigationHub.stateActions.resetTabIndex(0);
resetTabIndex(int tabIndex);

/// Change the current tab programmatically
/// E.g. MyNavigationHub.stateActions.currentTabIndex(2);
currentTabIndex(int tabIndex);

/// Update the badge count
/// E.g. MyNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 2);
updateBadgeCount({required int tab, required int count});

/// Increment the badge count
/// E.g. MyNavigationHub.stateActions.incrementBadgeCount(tab: 0);
incrementBadgeCount({required int tab});

/// Clear the badge count
/// E.g. MyNavigationHub.stateActions.clearBadgeCount(tab: 0);
clearBadgeCount({required int tab});

/// Enable the alert for a tab
/// E.g. MyNavigationHub.stateActions.alertEnableTab(tab: 0);
alertEnableTab({required int tab});

/// Disable the alert for a tab
/// E.g. MyNavigationHub.stateActions.alertDisableTab(tab: 0);
alertDisableTab({required int tab});

/// Navigate to the next page in a journey layout
/// E.g. await MyNavigationHub.stateActions.nextPage();
Future<bool> nextPage();

/// Navigate to the previous page in a journey layout
/// E.g. await MyNavigationHub.stateActions.previousPage();
Future<bool> previousPage();

To use a state action, you can do the following:

MyNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 2);

MyNavigationHub.stateActions.resetTabIndex(0);

MyNavigationHub.stateActions.currentTabIndex(2); // Switch to tab 2

await MyNavigationHub.stateActions.nextPage(); // Journey: go to next page

Loading Style

Out the box, the Navigation Hub will show your default loading Widget (resources/widgets/loader_widget.dart) when the tab is loading.

You can customize the loadingStyle to update the loading style.

Style Description
normal Default loading style
skeletonizer Skeleton loading style
none No loading style

You can change the loading style like this:

@override
LoadingStyle get loadingStyle => LoadingStyle.normal();
// or
@override
LoadingStyle get loadingStyle => LoadingStyle.skeletonizer();

If you want to update the loading Widget in one of the styles, you can pass a child to the LoadingStyle.

@override
LoadingStyle get loadingStyle => LoadingStyle.normal(
    child: Center(
        child: Text("Loading..."),
    ),
);

Now, when the tab is loading, the text "Loading..." will be displayed.

Example below:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    _MyNavigationHubState() : super(() async {

      await sleep(3); // simulate loading for 3 seconds

      return {
        0: NavigationTab.tab(
          title: "Home",
          page: HomeTab(),
        ),
        1: NavigationTab.tab(
          title: "Settings",
          page: SettingsTab(),
        ),
      };
    });

    @override
    LoadingStyle get loadingStyle => LoadingStyle.normal(
        child: Center(
            child: Text("Loading..."),
        ),
    );
    ...
}

Creating a Navigation Hub

To create a Navigation Hub, you can use Metro, use the below command.

metro make:navigation_hub base

The command will guide you through an interactive setup where you can choose the layout type and define your tabs or journey states.

This will create a base_navigation_hub.dart file in your resources/pages/navigation_hubs/base/ directory with child widgets organized in tabs/ or states/ subfolders.