Flutter go_router: The Essential Guide

António Nicolau
7 min readJan 29, 2023

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 routing options provided by Flutter. It can be useful if you want more control over how routes are defined and managed in your app. It also has a good support for web so that’s a nice choice for your application.

You can define URL patterns, navigate using a URL, handle deep links, and a number of other navigation-related scenarios.

Features

GoRouter has a number of features to make navigation straightforward:

  • Parsing path and query parameters using a template syntax
  • Displaying multiple screens for a destination (sub-routes)
  • Redirection support — you can redirect the user to a different URL based on application state, for example to a sign-in when the user is not authenticated
  • Support Nested Tab navigation with StatefulShellRoute
  • Support for both Material and Cupertino apps
  • Backwards-compatibility with Navigator API

Get started

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

dependencies:
go_router: ^7.1.1

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: 'page2',
path: '/page2',
builder: (context, state) => Page2Screen(),
),
],
);

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 around 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.params['id'] // Get "id" param from URL
return FruitsPage(id: id);
},
),

We can also access query string parameter using GoRouterState. For example, a URL path such as /fruits?search=antonio can read the search parameter:

GoRoute(
path: '/fruits',
builder: (context, state) {
final search = state.queryParams['search'];
return FruitsPage(search: search);
},
),

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 query parameters, you can use the Uri class:

context.go(
Uri(
path: '/fruit-detail',
queryParameters: {'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.

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 Home ==> Shope)
  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 👇🏿

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),
);

Type-safe routes

Instead of using URL strings (context.go(“/auth”)) to navigate, go_router supports type-safe routes using the go_router_builder package.

To get started, add go_router_builder, build_runner, and build_verify to the dev_dependencies section of your pubspec.yaml:

dev_dependencies:
go_router_builder: ^1.0.16
build_runner: ^2.3.3
build_verify: ^3.1.0

Defining a route

Then define each route as a class extending GoRouteData and overriding the build method.

class HomeRoute extends GoRouteData {
const HomeRoute();

@override
Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

Route tree

The tree of routes is defined as an attribute on each of the top-level routes:

import 'package:go_router/go_router.dart';

part 'go_router.g.dart'; // name of generated file

// Define how your route tree (path and sub-routes)
@TypedGoRoute<HomeScreenRoute>(
path: '/home',
routes: [ // Add sub-routes
TypedGoRoute<SongRoute>(
path: 'song/:id',
)
]
)

// Create your route screen that extends "GoRouteData" and @override "build"
// method that return the screen for this route
@immutable
class HomeScreenRoute extends GoRouteData {
@override
Widget build(BuildContext context) {
return const HomeScreen();
}
}

@immutable
class SongRoute extends GoRouteData {
final int id;
const SongRoute({required this.id});

@override
Widget build(BuildContext context) {
return SongScreen(songId: id.toString());
}
}

To build the generated files use the build_runner command:

flutter pub global activate build_runner // Optional, if you already have build_runner activated so you can skip this step
flutter pub run build_runner build

To navigate, construct a GoRouteData object with the required parameters and call go():

TextButton(
onPressed: () {
const SongRoute(id: 2).go(context);
},
child: const Text('Go to song 2'),
),

Before you go !!!

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.

Find here the project example 👇🏿👇🏿

Hope you enjoyed the trip to go_router 😊🥳

Do you know you can clap up to 50 times for an article? Go give it a try!

--

--

António Nicolau

Make learning Android, iOS, and software development more fun and easier 💻🖱️