← Back to list

Flutter Go Router : The Crucial Guide

Go_router is a third-party package for routing in Flutter that aims to provide a more flexible and easy-to-use solution than the default…

Vipin Mehra · 2024-02-25 08:11 · 880 claps · 6.4 min read
#flutter #go-router #nested-navigation #routing #deep-linking
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Flutter Go Router : The Crucial Guide

Go_router is a third-party package for routing in Flutter that offers a more flexible and user-friendly alternative to Flutter’s default routing options. It provides enhanced control over route definitions and management within your app.

With go_router, you can define URL patterns, navigate using URLs, handle deep links, and address various navigation-related scenarios.

Features

GoRouter has a number of features to make navigation straightforward:

  • Backward compatibility with Navigator API: Seamlessly integrates with existing Navigator-based code.
  • Support for Material and Cupertino apps: Works with both design languages to ensure a consistent look and feel.
  • Nested tab navigation with StatefulShellRoute: Allows for complex navigation scenarios within tabs.
  • Displaying multiple screens for a destination (sub-routes): Facilitates detailed routing structures within your app.
  • Parsing path and query parameters using template syntax: Simplifies handling of dynamic routes and parameters.
  • Redirection support: Redirect users based on application state, such as sending unauthenticated users to a sign-in page.

[embed]

Get started

To get started, add go_router to your pubspec.yaml. In this article we’ll be using ^13.2.0

dependencies:
  go_router: ^13.2.0

Route Configuration

After doing that lets add GoRouter configuration to your app:

import 'package:go_router/go_router.dart';

// GoRouter configuration
final _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      name: 'home', // Optional, add name to your routes. Allows you navigate by name instead of path
      path: '/',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      name: 'shope',
      path: '/shope',
      builder: (context, state) => ShopeScreen(),
    ),
  ],
);

Then we can use either the MaterialApp.router or CupertinoApp.router constructor and set the routerConfig parameter to your GoRouter configuration object:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

That’s it 🙂 you’re ready to play with go_router !!!

Parameters

To specify a path parameter, prefix a path segment with a : character, followed by a unique name, for example, :userId. We access the parameter value by GoRouterState object provided to the builder callback:

GoRoute(
  path: '/fruits/:id',
  builder: (context, state) {
     final id = state.pathParameters["id"]! // Get "id" param from URL
     return FruitsPage(id: id);
  },
),

Adding child routes

A matched route can result in more than one screen being displayed on a Navigator. This is equivalent to calling push(), where a new screen is displayed above the previous screen, and an in-app back button in the AppBar widget is provided.

To do it we add a child route and its parent routes:

GoRoute(
  path: '/fruits',
  builder: (context, state) {
    return FruitsPage();
  },
  routes: <RouteBase>[ // Add child routes
    GoRoute(
      path: 'fruits-details', // NOTE: Don't need to specify "/" character for router’s parents
      builder: (context, state) {
        return FruitDetailsPage();
      },
    ),
  ],
)

Navigation Between Screens

There are many ways to navigate between destinations with go_router.

To change to a new screen, call context.go() with a URL:

build(BuildContext context) {
  return TextButton(
    onPressed: () => context.go('/fruits/fruit-detail'),
  );
}

We can also navigate by name instead of URL, call context.goNamed()

build(BuildContext context) {
  return TextButton(
    // remember to add "name" to your routes
    onPressed: () => context.goNamed('fruit-detail'),
  );
}

To build a URI with path parameters, you can use the Uri class:

context.go(
  Uri(
    path: '/fruit-detail',
   pathParameters: {'id': '10'},
   ).toString(),
);

We can pop the current screen via context.pop().

Nested Tab navigation

Some apps display destinations in a subsection of the screen, for example, a BottomNavigationBar that stays on-screen when navigating between Screens.

We set up nested navigation using StatefulShellRoute.

This StatefulShellRoute class places its sub-route on a different Navigator than the root Navigator. However, this route class differs in that it creates separate Navigators for each of its nested branches (i.e. parallel navigation trees), making it possible to build an app with stateful nested navigation.

This is convenient when for instance implementing a UI with a BottomNavigationBar, with a persistent navigation state for each tab.

A StatefulShellRoute is created by specifying a List of StatefulShellBranch items, each representing a separate stateful branch in the route tree. StatefulShellBranch provides the root routes and the Navigator key (GlobalKey) for the branch and an optional initial location.

Let’s see how to implement it 🙂

We start by creating our router, we’re going to add StatefulShellRoute.indexedStack() to our routes, this class is going to be responsible to create our nested navigation.

StatefulShellRoute.indexedStack() constructs a StatefulShellRoute that uses an IndexedStack for its nested Navigators.

This constructor provides an IndexedStack based implementation for the container (navigatorContainerBuilder) used to manage the Widgets representing the branch Navigators.

// Create keys for `root` & `section` navigator avoiding unnecessary rebuilds
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _sectionNavigatorKey = GlobalKey<NavigatorState>();

final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/home',
  routes: <RouteBase>[
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        // Return the widget that implements the custom shell (e.g a BottomNavigationBar).
        // The [StatefulNavigationShell] is passed to be able to navigate to other branches in a stateful way.
        return ScaffoldWithNavbar(navigationShell);
      },
      branches: [
        // The route branch for the 1º Tab
        StatefulShellBranch(
          navigatorKey: _sectionNavigatorKey,
          // Add this branch routes
          // each routes with its sub routes if available e.g feed/uuid/details
          routes: <RouteBase>[
            GoRoute(
              path: '/shope',
              builder: (context, state) => const ShopePage(),
              routes: <RouteBase>[
                GoRoute(
                  path: 'detail',
                  builder: (context, state) => const FeedDetailsPage(),
                )
              ],
            ),
          ],
        ),

        // The route branch for 2º Tab
        StatefulShellBranch(routes: <RouteBase>[
          // Add this branch routes
          // each routes with its sub routes if available e.g shope/uuid/details
          GoRoute(
            path: '/home',
            builder: (context, state) => const HomePage(),
          ),
        ])
      ],
    ),
  ],
);

We added StatefulShellRoute.indexedStack() to our route, it’s responsible to create our branches and return a custom shell (in this case a BottomNavigationBar).

  1. In the builder: (context, state, navigationShell) we return our custom shell, basically a Scaffold with a BottomNavigationBar, remember to pass navigationShell to this page since we’ll use that to navigate to others branch (e.g Shope ==> Home)
  2. In the branches:[] we give a list of StatefulShellBranch (our branches). We pass our previous created _sectionNavigatorKey to navigatorKey property but just for the first branch, a default key will be used for others branches. We also give it a list of RouteBase ( the supported routes for that branch)

As you could see our builder return our custom shell that contains our BottomNavigationBar so let’s create that 👇🏿

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

class ScaffoldWithNavbar extends StatelessWidget {
  const ScaffoldWithNavbar(this.navigationShell, {super.key});

  /// The navigation shell and container for the branch Navigators.
  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shope'),
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
        ],
        onTap: _onTap,
      ),
    );
  }

  void _onTap(index) {
    navigationShell.goBranch(
      index,
      // A common pattern when using bottom navigation bars is to support
      // navigating to the initial location when tapping the item that is
      // already active. This example demonstrates how to support this behavior,
      // using the initialLocation parameter of goBranch.
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

Basically we return a Scaffold with BottomNavigationBar, the body is going to be a navigationShell that we got from our router.

There’s also an _onTap(index) , here we use navigationShell.goBranch(index) this way we can change between branches.

And that’s it, you’re ready to implement that in your projects 🥳🎉

For a complete example checkout my repository below 👇🏿

Guards

To guard specific routes, e.g. from un-authenticated users, global redirect can be set up via GoRouter. A most common example would be the set up redirect that guards any route that is not /login and redirects to /login if the user is not authenticated

A redirect is a callback of the type GoRouterRedirect. To change incoming location based on some application state, add a callback to either the GoRouter or GoRoute constructor:

GoRouter(
  redirect: (BuildContext context, GoRouterState state) {
    final isAuthenticated = // your logic to check if user is authenticated
    if (!isAuthenticated) {
      return '/login';
    } else {
      return null; // return "null" to display the intended route without redirecting
     }
   },
  ...
  • You can define redirect on the GoRouter constructor. Called before any navigation event.
  • Define redirect on the GoRoute constructor. Called when a navigation event is about to display the route.

You can specify a redirectLimit to configure the maximum number of redirects that are expected to occur in your app. By default, this value is set to 5. GoRouter will display the error screen if this redirect limit is exceeded

Transition animations

GoRouter allows you to customise the transition animation for each GoRoute. To configure a custom transition animation, provide a pageBuilder parameter to the GoRoute constructor:

GoRoute(
  path: '/fruit-details',
  pageBuilder: (context, state) {
    return CustomTransitionPage(
      key: state.pageKey,
      child: FruitDetailsScreen(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        // Change the opacity of the screen using a Curve based on the the animation's value
        return FadeTransition(
          opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation),
          child: child,
        );
      },
    );
  },
),

For a complete example, see the transition animations sample.

Error handling (404 page)

By default, go_router comes with default error screens for both MaterialApp and CupertinoApp as well as a default error screen in the case that none is used. You can also replace the default error screen by using the errorBuilder parameter:

GoRouter(
  /* ... */
  errorBuilder: (context, state) => ErrorPage(state.error),
);

Before You Goo !!

There’s still a nice feature with go_router, you can add a NavigatorObserver to our GoRouter for observing the behavior of a Navigator, listen for whenever a route was push, pop or replace. To do so let’s create a class that extends NavigatorObserver :

class MyNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    log('did push route');
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    log('did pop route');
  }

Now lets add MyNavigatorObserver to our GoRouter

GoRouter(
  ...
  observers: [ // Add your navigator observers
    MyNavigatorObserver(),
  ],
...
)

Whenever those events are triggered your navigator will be notified.

[embed]


메타데이터
post_id
41dc615045bb
slug
flutter-go-router-the-crucial-guide-41dc615045bb
url
https://medium.com/@vimehraa29/flutter-go-router-the-crucial-guide-41dc615045bb
canonical_url
https://medium.com/@vimehraa29/flutter-go-router-the-crucial-guide-41dc615045bb
author_url
https://medium.com/@vimehraa29
status
ok
fetched_at
2026-08-02 05:07:27