The Ultimate Guide to GoRouter: Navigation in Flutter Apps Part -2 (Nested Routers, Redirect, Guard, Error Handling)

Harish KunchalaHarish Kunchala
7 min read

You can find the Part 1 here: https://tinyurl.com/k3f64hwv

Dashboard Page - Nested Routes

Example 1: Tabbed Content with Widgets

Nested routes are useful for organizing complex user interfaces with multiple layers of navigation, such as a dashboard with multiple sections, each represented as a tab or a sub-page. This approach helps maintain a clean and manageable structure in your routing setup.

Step 1: Define the DashboardPage Widget

Let's create our DashboardPage with three tabs: Home, Settings, and Profile. Each tab will represent a nested route.

import 'package:flutter/material.dart';

class DashboardPage extends StatelessWidget {
  const DashboardPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Dashboard'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: 'Home'),
              Tab(icon: Icon(Icons.settings), text: 'Settings'),
              Tab(icon: Icon(Icons.account_circle), text: 'Profile'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            Center(child: Text('Home Tab Content')),
            Center(child: Text('Settings Tab Content')),
            Center(child: Text('Profile Tab Content')),
          ],
        ),
      ),
    );
  }
}

Step 2: Configure Nested Routes in AppRouter

Now, let's set up the routes in the AppRouter. Although the above DashboardPage uses DefaultTabController for simplicity, nested routes in GoRouter would involve setting up child routes under a parent route.

GoRoute(
          path: '/dashboard',
          builder: (BuildContext context, GoRouterState state) =>
              const DashboardPage(),
          routes: [
            GoRoute(
              path: 'home',
              builder: (BuildContext context, GoRouterState state) =>
                  const Center(child: Text("Home Tab Content")),
            ),
            GoRoute(
              path: 'settings',
              builder: (BuildContext context, GoRouterState state) =>
                  const Center(child: Text("Settings Tab Content")),
            ),
            GoRoute(
              path: 'profile',
              builder: (BuildContext context, GoRouterState state) =>
                  const Center(child: Text("Profile Tab Content")),
            ),
          ]),

Explanation

  • Parent Route (/dashboard): This is the main dashboard page.

  • Nested Routes: Under the dashboard, we have three sub-routes (home, settings, profile). Each of these could potentially be fleshed out with more complex functionality or screens.

Step 3: Testing the Setup

To test this setup:

  1. Let's create an ElevatedButton on HomePage to go to Dashboard

  2. Click on the tabs and observe that the content changes according to the selected tab.

Nested Routes on Phone

  1. We can also directly navigate to /dashboard/home, /dashboard/settings, or /dashboard/profile to see if the respective content is shown.

Example 2: Tabbed Content with Actual Screens

So let's create an all the screens to be put in the tabs. I am going to go with basic UI but if you want you can additional features inside each screen too.

Step 1: Create Specific Screens for Each Tab

First, let's define separate widgets for each tab to be used in the nested routes:

import 'package:flutter/material.dart';

// HomeTabScreen with a unique background color
class HomeTabScreen extends StatelessWidget {
  const HomeTabScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue[100], // Light blue background
      child: const Center(child: Text('Home Tab Content')),
    );
  }
}

// SettingsTabScreen with a unique background color
class SettingsTabScreen extends StatelessWidget {
  const SettingsTabScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green[100], // Light green background
      child: const Center(child: Text('Settings Tab Content')),
    );
  }
}

// ProfileTabScreen with a unique background color
class ProfileTabScreen extends StatelessWidget {
  const ProfileTabScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.purple[100], // Light purple background
      child: const Center(child: Text('Profile Tab Content')),
    );
  }
}

Step 2: Update theDashboardPage to Include Specific Screens

Modify the DashboardPage to use these new widgets within the TabBarView:

class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text("Dashboard"),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: "Home"),
              Tab(icon: Icon(Icons.settings), text: "Settings"),
              Tab(icon: Icon(Icons.account_circle), text: "Profile"),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            HomeTabScreen(),
            SettingsTabScreen(),
            ProfileTabScreen(),
          ],
        ),
      ),
    );
  }
}

Step 3: Integrate with GoRouter

Now, integrate these screens into your GoRouter setup. Since we want to use GoRouter to handle the actual switching between tabs based on the route (rather than just using DefaultTabController), so let's set up child routes:

 GoRoute(
          path: '/dashboard',
          builder: (BuildContext context, GoRouterState state) =>
              const DashboardPage(),
          routes: [
            GoRoute(
              path: 'home',
              builder: (BuildContext context, GoRouterState state) =>
                  const HomeTabScreen(),
            ),
            GoRoute(
              path: 'settings',
              builder: (BuildContext context, GoRouterState state) =>
                  const SettingsTabScreen(),
            ),
            GoRoute(
              path: 'profile',
              builder: (BuildContext context, GoRouterState state) =>
                  const ProfileTabScreen(),
            ),
          ])

Step 4: Testing the Setup

Run your application:

Web View of the Dashboard

You can find the entire source code for nested routes here: https://github.com/khkred/flutter_comp_go_router/tree/nested_routes

Concept: Redirection and Guards

  • Guards in GoRouter allow you to perform checks before a route is shown to the user.

  • If the check fails, you can redirect the user to another route, typically used for scenarios like redirecting unauthenticated users to a login page.

Example Scenario

Let's implement a simple authentication check where users need to be logged in to view the DashboardPage. If they are not authenticated, they will be redirected to a LoginPage.

Step 1: Define the LoginPage

First, let’s create a LoginPage that provides an option to "log in" for simplicity.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Login Page")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Please log in to continue'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                //Simulate a login by setting a logged in flag
                context.go('/dashboard');
              },
              child: const Text('Log In'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 2: Setup Authentication Guard

Let’s define a simple mechanism to check if a user is logged in. For demonstration, we'll use a global variable (Note: In production, you should use a more secure method, such as a state management solution or secure storage, which we'll see in later lessons when we use Riverpod).
Since this is a global variable. For now let's declare it in a global.dart file

bool isLoggedIn = false;  // This will be our simple auth check

Now let's add a guard to the AppRouter:

 GoRoute(
        path: '/login',
        builder: (BuildContext context, GoRouterState state) =>
            const LoginPage(),
      ),

      GoRoute(
          path: '/dashboard',
          builder: (BuildContext context, GoRouterState state) =>
              const DashboardPage(),
          redirect: (BuildContext context, GoRouterState state) {
            if (!isLoggedIn) {
              return '/login'; // Redirect to login if not authenticated
            }
            return null; // No redirection if authenticated
          },

Now let's run the code (Remember Right now isLoggedIn = false )

Dashboard when isLogged false

Now let's set isLoggedIn = true and then run the app

When isLoggedIn true

You can find the source code for Redirection and Guards here: https://github.com/khkred/flutter_comp_go_router/tree/redirection_and_guards

Concept: Error Handling

GoRouter allows us to define a custom error page that can be shown when no matching routes are found or when an exception occurs during navigation. This is crucial for preventing users from encountering raw error messages and instead provides them with helpful information or actions they can take.

Step 1: Define an ErrorPage

Let's create an ErrorPage that will display error information. This page will show a message and provide an option to navigate back to a safe place like the home page.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class ErrorPage extends StatelessWidget {
  final String? error;

  const ErrorPage({super.key, this.error});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Error')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(error ?? 'Something went wrong!', style: const TextStyle(color: Colors.red, fontSize: 18)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                context.go('/'); // Navigate back to the home page
              },
              child: const Text('Go Home'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 2: Integrate Error Handling in AppRouter

Now, integrate this ErrorPage into your AppRouter to handle any route errors. You'll set up a global error builder that GoRouter will call whenever navigation to a route fails.

static final GoRouter router = GoRouter(
    errorBuilder: (BuildContext context, 
GoRouterState state) => ErrorPage(
      error: state.error?.toString(),
    ),
    routes: [...]
);

Explanation:

  • Error Builder: The errorBuilder is a callback that gets triggered whenever there is a navigation error. It receives the error state, which includes details about what went wrong.

  • Usage of ErrorPage: The ErrorPage is used to display the error. If there's a specific error message available from the navigation attempt, it displays that message; otherwise, it shows a generic "Something went wrong!" message.

Step 3: Testing the Setup

To effectively test error handling:

  1. Intentionally Navigate to a Non-Existent Route: Try navigating to a route that does not exist in your app, such as /thisdoesnotexist.

  2. Check the Error Page: Verify that the ErrorPage is displayed, and it provides the appropriate error message or a generic message.

  3. Use the Button: Click on the "Go Home" button to ensure it navigates back to the home page correctly.

Error Page

You can find the source code for Error Handling here: https://github.com/khkred/flutter_comp_go_router/tree/error_handling

Thanks a lot for reading. Good Luck with your Flutter Journey.

0
Subscribe to my newsletter

Read articles from Harish Kunchala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harish Kunchala
Harish Kunchala