The Ultimate Guide to GoRouter: Navigation in Flutter Apps Part -1 (Go Router Setup, Declarative Routing, Type Safety , Path and Query Params)

The Ultimate Guide to GoRouter: Navigation in Flutter Apps Part -1 (Go Router Setup, Declarative Routing, Type Safety , Path and Query Params)

As a Flutter developer, you're likely no stranger to the importance of navigation in your app. Whether you're building a simple to-do list or a complex e-commerce platform, navigation is a crucial aspect of providing a seamless user experience.

In this article, we'll dive into the world of GoRouter, a powerful navigation library for Flutter that makes it easy to manage your app's navigation flow.

What is GoRouter?

GoRouter is an open-source navigation library for Flutter that provides a simple and efficient way to manage your app's navigation. Developed by the Flutter team, GoRouter is designed to simplify the process of navigating between screens, handling route changes, and managing the app's navigation stack.

Why Use GoRouter? All the Features comprehensively: We are going to cover every single one in this article.

  1. Declarative Routing:

    • GoRouter allows you to define routes in a declarative manner, which means all routing logic is centralized and not scattered throughout the application. This makes it easier to understand the navigation flow and modify it.
  2. Built-in Deep Linking Support:

    • Handling deep links is more straightforward with GoRouter, as it seamlessly integrates deep linking into its navigation system. This simplifies the process of linking to specific screens from external sources.
  3. Type Safety:

    • GoRouter supports strong typing for route parameters, reducing runtime errors and enhancing code reliability. This strong typing ensures that parameters required by different screens are correctly passed and interpreted.
  4. Simplified Handling of Nested Routes:

    • Managing nested routes (routes within routes) is much easier with GoRouter. This is particularly useful in complex applications with multiple layers of navigation, such as tabs within stacks.
  5. Redirection and Guards:

    • GoRouter provides built-in support for redirection and guards. Guards are useful for implementing requirements like user authentication before accessing certain parts of an app. Redirection helps in controlling the navigation flow based on specific conditions (e.g., redirecting to a login screen if the user is not authenticated).
  6. Error Handling:

    • It offers robust error handling capabilities, allowing developers to manage unknown routes or errors within the routing system effectively.
  7. Programmatic and Declarative Navigation:

    • While GoRouter is primarily declarative, it also supports programmatic navigation where necessary, giving developers the best of both worlds.
  8. Performance:

    • With its efficient management of route states and changes, GoRouter can lead to better performance in complex applications, where managing the navigation stack can otherwise become.

Starter Code:

This is the starter code of the app: https://github.com/khkred/flutter_comp_go_router/tree/starter-code

This is the current tree of the screens inside the starter code

lib/screens/
├── dashboard_page.dart
├── deep_link_page.dart
├── details_page.dart
├── error_page.dart
├── home_page.dart
├── login_page.dart
├── profile_page.dart
└── settings_page.dart

1 directory, 8 files

Each screen will have a specific purpose in the future:

  1. HomePage - Declarative Routing

  2. Details Page - Type Safety and Parameters

  3. Settings Page - Nested Routes

  4. Profile Page - Type Safety and Parameters

  5. Login Page - Redirection and Guards

  6. Dashboard Page - Nested Routes

  7. Error Page - Error Handling

  8. Deep Link Page - Built-in Deep Linking Support

Setup Declarative Routing:

First we have to setup the Go Router in our app. Our GoRouter package is already added into the Starter Code. But If you want to add it into your pubspec.yaml. Run the following command in your terminal:

flutter pub add go_router

Now that the GoRouter is added. Let's setup the initial navigation for GoRouter in a separate file named app_router.dart . Here is the code:

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

class AppRouter {
  static final GoRouter router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) => const HomePage(),
      ),
    ],
  );
}

Explanation:

  • GoRouter Initialization: We create a static final instance of GoRouter named router. This instance is initialized with a list of routes.

  • Route Definition: The GoRoute constructor is used to define a route.

    • path: This specifies the path in the URL. Here, '/' denotes the root or home route.

    • builder: This function returns the widget that should be displayed when navigating to this route. In this case, it returns an instance of HomePage.

UsingAppRouter in Your Main Application:

To use the AppRouter in your main application, modify your main.dart file to utilize the GoRouter instance for routing:

import 'package:flutter/material.dart';
import 'app_router.dart';
void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Go Router Example',
      routerConfig: AppRouter.router,
    );
  }
}

Perfect. So now when we open the app it directly goes to the homepage:

Declarative Routing

So this covers Declarative Routing. You can find the total code here: https://github.com/khkred/flutter_comp_go_router/tree/declarative-routing

Type Safety and Parameters. Part -1 (Path Parameters)

Example 1: DetailsPage

Let's set up the Details Page to demonstrate type safety and parameter handling using GoRouter. We'll create a route that requires a parameter, such as an ID, to fetch and display specific details on the Details Page.

Step 1: Update theDetailsPage Widget

First, let’s modify the DetailsPage to accept a parameter, say an item ID, which it will use to display specific content.

import 'package:flutter/material.dart';

class DetailsPage extends StatelessWidget {
  final int itemId;

  // Ensuring type safety with required parameters
  const DetailsPage({super.key, required this.itemId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details Page')),
      body: Center(
        child: Text('Displaying details for item ID: $itemId',
            style: const TextStyle(fontSize: 24)),
      ),
    );
  }
}

Step 2: Add the Route inAppRouter

Now, integrate this page into your AppRouter by defining a route that includes the parameter. You’ll capture the parameter from the URL and pass it to the DetailsPage.

Update your app_router.dart file like this:

import 'package:flutter/material.dart';
import 'package:flutter_comp_go_router/screens/details_page.dart';
import 'package:go_router/go_router.dart';
import 'screens/home_page.dart';

class AppRouter {
  static final GoRouter router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) =>
            const HomePage(),
      ),

      GoRoute(
          path: 'details/:itemId',
          builder: (BuildContext context, GoRouterState state) {
            final params = state.pathParameters['itemId'];
            final int itemId = int.tryParse(params ?? '') ?? 0;
            return DetailsPage(itemId: itemId);
          }),
    ],
  );
}

Now we are adding a textfield in the home_page.dart where a user can enter a number and the app will take you the details_page.dart with that id.

Here's the relevant function in home_page.dart with navigation:

  void _navigateToDetails() {
    if (_detailsPageIdController.text.isNotEmpty) {
      final itemId = int.tryParse(_detailsPageIdController.text);
      if (itemId != null) {
        //We are going to the details page with the item ID
        context.go('/details/$itemId');
      } else {
        // Error handling for non-numeric input
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('Error'),
            content: const Text('Please enter a valid number.'),
            actions: <Widget>[
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('OK'),
              ),
            ],
          ),
        );
      }
    }
  }

Type Safety and Params Details Page

But you cannot go back from the details page. If you want to go back then all you have to do is change

 context.go('/details/$itemId');

to

 context.push('/details/$itemId');

Go back to Homepage from Details Page

Example 2: ProfilePage Accessing from URL

The ProfilePage will demonstrate how to securely pass and handle route parameters, specifically focusing on ensuring type safety, which is crucial for preventing runtime errors and bugs related to data types.

Step 1: Update the ProfilePage Widget

Let's define the ProfilePage to accept a user ID as a parameter and display it. This is a basic example of type safety, as the page will expect an integer and handle it appropriately. This is extremely similar to DetailsPage but we want to see how we'll access it from the URL

import 'package:flutter/material.dart';

class ProfilePage extends StatelessWidget {
  final int userId;

  // Ensuring type safety with required integer parameter
  const ProfilePage({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile Page')),
      body: Center(
        child: Text('Profile for user ID: $userId'),
      ),
    );
  }
}

Step 2: Configure the Route in AppRouter

Now, integrate the ProfilePage into your routing setup in the AppRouter. This involves capturing a path parameter from the URL and passing it securely to the ProfilePage.

So let's go ahead and add the following Route to our routes in app_router.dart

GoRoute(
path: '/profile/:userId',
builder: (BuildContext context, GoRouterState state){
final params = state.pathParameters['userId'];
// Here we are making sure that the userId is an integer for Type Safety
final int userId = int.tryParse(params ?? '') ?? 0;
return ProfilePage(userId: userId);
      }),

In this setup:

  • Path Parameter Handling: The route uses :userId as a path parameter. This parameter is extracted from the URL and converted to an integer (int.tryParse). This ensures that the ID passed to ProfilePage is always an integer, maintaining type safety.

  • Error Handling:The fallback to 0 if parsing fails is a simple error handling mechanism.

Step 3: Testing the Setup

We want to test the app using URLS. So First let's add web support for our app. Since our current app only supports Android and iOS. Run the following command in our terminal

  1. Add Web Platform to our existing Flutter Project
flutter create --platforms web .
  1. Enable Web Support. Just to be safe restart your editor
flutter config --enable-web
  1. Run the project in chrome
flutter run -d chrome

Now after we run the app on Chrome. We can enter the URL

Profile Page Gif

Remove the # from URL:

If you wish to eliminate the ‘#’ symbol from your URL, you can use the usePathUrlStrategy class in Flutter.

So in our main.dart we have to add the following code

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

void main(){
  usePathUrlStrategy();
  runApp(MyApp());
}

You can find the final source code here: https://github.com/khkred/flutter_comp_go_router/tree/type_safety_and_parameters

Type Safety and Parameters. Part -2 (Query Parameters)

Definition: Query parameters are used to provide optional modifications or filter data on a given resource or page. They are not part of the URL path but are appended at the end after a ? mark.

Example:

GoRoute(
  path: '/users',
  builder: (context, state) => UsersScreen(filter: state.queryParameters['filter']),
),

Difference between Path Parameters and Query Parameters:
Here is a table describing the difference between the two:

Path ParameterQuery String Parameter
Syntax:userId?filter=admins
Accessstate.pathParameters['userId']state.uri.queryParameters['filter']
Example/users/:userId/users?filter=admins
PurposeSpecify a dynamic part of the URL pathSpecify a dynamic query string parameter
UsageUse in path to specify a dynamic valueUse in query string to specify a dynamic value

In summary, path parameters are used to specify dynamic parts of the URL path, while query string parameters are used to specify dynamic values in the query string.

Step 1: Define the Screen That Will Use Query Parameters

First, let's define a Flutter screen that can accept query parameters. For example, we might create a UsersScreen that can filter users based on a query parameter named filter. Here’s a basic implementation:

import 'package:flutter/material.dart';

class UsersScreen extends StatelessWidget {
  final String? filter;

  const UsersScreen({super.key, this.filter});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Users Screen')),
      body: Center(
        // Displaying the filter if it exists
        child: Text(filter != null ? 'Filtering by: $filter' : 'No filter applied'),
      ),
    );
  }
}

Step 2: Setup the Route with Query Parameters inAppRouter

Next, let's add a route to our AppRouter to handle a route that includes query parameters. Here’s how you might set it up:

// Pay Attention to this route as it has a query parameter and the filter is passed to the UsersScreen
GoRoute(path: '/users',
builder: (BuildContext context, GoRouterState state){
final String? filter = state.uri.queryParameters['filter']; 
return UsersScreen(filter: filter);
      }),

So this functions in the HomePage helps us navigate to the UsersPage()

void _navigateToUsers() {
    final filter = _filterController.text;
    if (filter.isNotEmpty) {
      context.push('/users?filter=$filter');
    } else {
      context.push('/users');
    }
  }

Query Parameters

Here's the current source code: https://github.com/khkred/flutter_comp_go_router/tree/query_parameters

Bonus Sections

Bonus 1: Setting an Initial Location

If we want to set an initial location for the app regardless of the root location we can do it by adding the initiaLocation:

First let's add a sample_page.dart to our app:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('This is the sample page, we got from initial Location'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                context.go('/'); // This will navigate to the home page
              },
              child: const Text('Go to Home Page'),
            ),
          ],
        ),
      ),
    );
  }
}

Now this is our updated router:


class AppRouter {
  static final GoRouter router = GoRouter(
    initialLocation: '/sample',
    routes: [
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) =>
            const HomePage(),
      ),
      GoRoute(path: '/sample',
        builder: (BuildContext context, GoRouterState state) => const SamplePage(),),
.........

This is what I get when I run the app right now:

initial location

As always you can find the link for the bonus code here: https://github.com/khkred/flutter_comp_go_router/tree/type_safety_and_parameters

Bonus -2: Named Routes

You can set a name for any route. So that we can call it instead of calling the entire path.

For instance let's give a name to our HomePage() route.

 GoRoute(
        name: 'home',
        path: '/',
        builder: (BuildContext context, GoRouterState state) =>
            const HomePage(),
      ),

Now I can navigate from our SamplePage() like this:

ElevatedButton(
              onPressed: () {
                context.goNamed('home'); // This will navigate to the home page
              },
              child: const Text('Go to Home Page'),
            ),

You can find the code here: named route commit

Bonus 3: Named Routes with Path Parameters

For instance let's set a name for our details route:

GoRoute(
        name: 'details',
        path: '/details/:itemId',
      ...),

Now if we want to call it via name. Let's modify the _navigateToDetails() in our home_page

//Old Navigation
 //context.push('/details/$itemId');

//New Navigation with named
        context.pushNamed(
          'details',
          pathParameters: {'itemId': '$itemId'},
        );
      }

Here's the total code: named route with path parameters commit

Bonus 4: Named Routes with Query Parameters

Let's add a name to our UsersScreen() route too:

GoRoute(
          path: '/users',
          name: 'users',
...),

Now we can update the _navigateToUsers() in our HomePage like this:

//Old Query Parameter
//context.push('/users?filter=$filter');

//New named query parameters
      context.pushNamed(
        'users',
        queryParameters: {'filter': filter},
      );

Here's the code: Named Route with Query Parameters Commit

Remaining Parts:

Here's the link for part 2: https://harishkunchala.com/the-ultimate-guide-to-gorouter-navigation-in-flutter-apps-part-2-nested-routers-redirect-guard-error-handling

Here's the link for part 3: https://harishkunchala.com/the-ultimate-guide-to-gorouter-navigation-in-flutter-apps-part-3-custom-transitions

Did you find this article valuable?

Support Harish Kunchala by becoming a sponsor. Any amount is appreciated!