Flutter Navigator 2.0: Using go_router

Go beyond Flutter’s Navigator 2.0 and learn how to handle navigation with the go_router package. By Kevin D Moore.

4.8 (12) · 7 Reviews

Download materials
Save for later
Share
Update note: Kevin D. Moore updated this tutorial for go_router. He also wrote the original. Special thanks to Chris Sells, the author of go_router, who helped review this tutorial.

The first version of this tutorial used a navigation system that’s pretty complicated.

Many developers — and even Google — realized the same thing. As a result, some developers wrote their packages to make the process easier. Google came out with a research paper evaluating three of the packages: VRouter, AutoRoute and Beamer.

All three of them have strengths and weaknesses. AutoRoute, for example, requires code generation. Beamer looked exciting, but it’s a bit confusing. VRouter was confusing to users due to similarly named APIs that were used in different contexts.

Another option is an intuitive and easy-to-use package called go_router.

Note: In Flutter, screens and pages are called routes. In this tutorial, you’ll see the screens and pages terms used. They mean the same thing for the most part.

In this tutorial, you’ll build a shopping app brilliantly called Navigation App. Through the process of building this app, you’ll learn:

  • How to implement Flutter Navigator 2.0 with the go_router navigation package.
  • How it can provide much more granular control for your app’s navigation.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Note: This tutorial uses Android Studio, but Visual Studio Code or IntelliJ IDEA will work fine as well.

The starter app is a set of screens for the shopping app. The UI doesn’t do much, but you’ll use it to navigate between pages. This set of pages — represented as screens — is in the image below:

Shopping App

Sneaking a Peek

The app starts with the Login page, which looks like this:

Login Screen

Run your app and verify it opens this page. The app will stay on this page since the navigation system isn’t yet implemented. Gradually, you’ll add code to navigate between all screens.

The flow will be:

  • Start at the Login screen.
  • From there, the user can log in or go to the Create Account screen.
  • At the Create Account screen, the user can create an account or go back to the Login screen.
  • Upon logging in, the user will be directed to the Home screen.
  • The Home screen will show three screens:
    • Shopping — A list of items. Selecting an item will display a details page.
    • Cart — Displays the current shopping cart.
    • Profile — This screen will show information related to payment, sign in and more.

Like many apps, this one requires the user to log in first before navigating around. So, you’ll want to prevent the user from going to the home screen until they have logged in.

You could check at every point that shows a page if the user has logged in and take them to the login page if they haven’t. Or, you can use some of the nice features of these routing packages to check the login state before showing a screen. Some packages call this feature guards. They guard against using a page unless the user is authorized. go_router uses the redirect callback for this purpose.

Introducing go_router

Google introduced a new Flutter routing system that requires customized RouterDelegate and RouterInformationParser classes. Both of these classes take a lot of work to implement and still leave you scratching your head. Many developers decided there was an easier way to handle routing. Some decided to use their system, and others plugged into Google’s router system.

The go_router package uses Google’s router system but makes it easy to use. There are two main classes you need to use:

  • GoRouter
  • GoRoute

Creating a GoRouter gives you a RouterDelegate and a RouterInformationParser for free. By creating this class, you can provide an initial route and the routes you need for each screen. There is even a section called redirect that allows you to use logic to decide which route to use.

This package works on all the main platforms: Android, iOS, Mac, Windows, Linux and the Web.

Examining GoRouter Components

The GoRouter class is made up of:

  • Routes
  • Error handler
  • Redirect handler

For routes, GoRouter uses a GoRoute. This class contains a path — like a URL, an optional name that you can use instead of paths and either a page builder that returns a page or a redirect handler that redirects to another route. GoRoutes can even have sub-routes. This is where you would put pages that the parent route would call.

For example, the profile page has four pages that it launches, so it uses a sub-route for those pages. Sub-routes are a way to create a stack of pages so that the back button is shown and you can go back to the parent page. This can be as deep as you like.

You can start creating routes with the path but using names is much easier. If you were to hard-code the names in your app, you’d see login if you are using names or /login if you are using paths. If you wanted to use a deeper path, like the details page, it would look something like /main/shop/details/item. It’s easier to use names so you can use details with a parameter.

To start implementing GoRouter, open pubspec.yaml and add the package:

  go_router: ^2.2.8

Now, click the Pub get link on the top right, or from the command line type:
flutter pub get.

Implementing Router

Create a new folder in the lib directory named router. Next, create a new dart file named routes.dart. Add the following imports. This includes all the screens and the go_router package:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../ui/create_account.dart';
import '../ui/error_page.dart';
import '../ui/home_screen.dart';
import '../ui/more_info.dart';
import '../ui/payment.dart';
import '../ui/personal_info.dart';
import '../ui/signin_info.dart';
import '../constants.dart';
import '../login_state.dart';
import '../ui/login.dart';
import '../ui/details.dart';

Now, create the MyRouter class:

class MyRouter {
  // 1
  final LoginState loginState;
  MyRouter(this.loginState);

  // 2
  late final router = GoRouter(
    // 3
    refreshListenable: loginState,
    // 4
    debugLogDiagnostics: true,
    // 5
    urlPathStrategy: UrlPathStrategy.path,

    // 6
    routes: [
      // TODO: Add Routes
    ],
    // TODO: Add Error Handler
    // TODO Add Redirect
  );

}

Here’s what’s happening in the code above:

  1. LoginState stores the user’s logged in state.
  2. You create a variable that holds a GoRouter instance.
  3. Then, you set the router to listen for changes to the loginState.
  4. Show debugging logs.
  5. Choose the path url strategy (can use hash ‘#’).
  6. Define all of the routes you’ll use.

There are several properties you can set for GoRouter. Here, you want the router to listen for changes in the login state. If the user logs out, you want the Login screen to appear. If they log in, the Home screen should appear. The debugLogDiagnostics flag is useful to see what path you’re using and to debug any problems with your routes.

Note: Make sure you remove the debugLogDiagnostics flag before shipping your app.

This defines the basics for using GoRouter. Now, you’ll need three things:

  1. A list of all the routes to your screens.
  2. Error Handler. If a route comes in (maybe from a deep link), handle an invalid route.
  3. Any redirect logic needed for redirecting to different pages based on any current state (optional).

Adding Routes

Now, start by adding the first routes for your screens. Note that all the paths are defined as strings in the constants.dart file. Replace // TODO: Add Routes with:

GoRoute(
   name: rootRouteName,
   path: '/',
   redirect: (state) =>
   // TODO: Change to Home Route
    state.namedLocation(loginRouteName),
),
GoRoute(
    name: loginRouteName,
    path: '/login',
    pageBuilder: (context, state) => MaterialPage<void>(
       key: state.pageKey,
       child: const Login(),
    ),
),
GoRoute(
    name: createAccountRouteName,
    path: '/create-account',
    pageBuilder: (context, state) => MaterialPage<void>(
       key: state.pageKey,
       child: const CreateAccount(),
    ),
),
// TODO: Add Home route and children

In the code above, you define the default route, which is the '/' path, and routes for the Login and Create Account screens. The default path will redirect you to the home route with the default shop tab selected.

For each route, you need to provide a:

  1. Path — This is a string in the form of /path.
  2. Name — defined in the constants.dart file (optional).
  3. Screen widget — defined in a page builder or a redirect.

Next, define the error handler. Replace // TODO: Add Error Handler with:

errorPageBuilder: (context, state) => MaterialPage<void>(
  key: state.pageKey,
  child: ErrorPage(error: state.error),
),

This calls the ErrorPage screen (defined in the ui folder) and passes in the exception defined by state.error. Next, you’ll have to provide the MyRouter class at the top of the widget tree.

Updating Main

Open main.dart in the lib directory.

In the import section, remove the import ui/login.dart import and add:

import 'router/routes.dart';

Find and replace // TODO: Add Provider with:

Provider<MyRouter>(
  lazy: false,
  create: (BuildContext createContext) =>
      MyRouter(loginState),
),

The code above will create your MyRouter class and provide it below.

Find and replace // TODO: Add Router with:

final router = Provider.of<MyRouter>(context, listen: false).router;
return MaterialApp.router(
  routeInformationParser: router.routeInformationParser,
  routerDelegate: router.routerDelegate,

This gets your router class from Provider and uses the routeInformationParser and routerDelegate that GoRouter provides. No need to create these yourself. You should now have two return statements.

Copy the fields from old MaterialApp object and paste them into your newly created object. Make sure to find // TODO: Remove and remove home: Login(),. Your return should look like the following:

final router = Provider.of<MyRouter>(context, listen: false).router;
return MaterialApp.router(
  routeInformationParser: router.routeInformationParser,
  routerDelegate: router.routerDelegate,
    debugShowCheckedModeBanner: false,
  title: 'Navigation App',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
);

After this, your build method will look like this:

Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider<CartHolder>(
        lazy: false,
        create: (_) => CartHolder(),
      ),
      ChangeNotifierProvider<LoginState>(
        lazy: false,
        create: (BuildContext createContext) => loginState,
      ),
      Provider<MyRouter>(
        lazy: false,
        create: (BuildContext createContext) => MyRouter(loginState),
      ),
    ],
    child: Builder(
      builder: (BuildContext context) {
        final router = Provider.of<MyRouter>(context, listen: false).router;
        return MaterialApp.router(
          routeInformationParser: router.routeInformationParser,
          routerDelegate: router.routerDelegate,
          debugShowCheckedModeBanner: false,
          title: 'Navigation App',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
        );
      },
    ),
  );
}

The last step before you see the magic of GoRouter is to implement these routes.

Implementing Routes

Open login.dart in the ui directory. In the import section, add:

import 'package:go_router/go_router.dart';

Find and replace // TODO: Add Create Account Route with:

context.goNamed(createAccountRouteName);

In the code above, you navigate to the Create Account screen.

GoRouter has a nice extension to the build context that allows you to call go or goNamed with the given route. Note that there are two different methods you can call:

  • go or goNamed: Replaces the current page with a new stack of the given page
  • push or pushNamed: Adds a page to the stack

Open create_account.dart in the ui directory. In the import section, add:

import 'package:go_router/go_router.dart';

Find and replace // TODO: Add Login Route with:

context.goNamed(loginRouteName);

In the code above, you navigate to the Login screen.

Hot restart. Make sure that when you tap Create Account from the Login screen, you go to the Create Account screen. And, make sure that when you tap Cancel from the Create Account screen, it takes you back to the Login screen.

Routing from Login to Create Account screen and vice-versa

From the Login or the Create Account screens, you’ll want to go to the Home screen when the user logs in or creates an account. Take a look at the saveLoginState method in login.dart file:

void saveLoginState(BuildContext context) {
  Provider.of<LoginState>(context, listen: false).loggedIn = true;
}

Here, you change the loggedIn flag to true. Since the router is listening to the LoginState class, it will refresh and display the Home screen. This won’t work yet since you haven’t hooked up the home route.

Using Redirects

Many apps have a login system. You can use the redirect section to handle the different states of your app. In this app, you’ll only deal with whether the user is logged in or not.

Return to routes.dart and replace // TODO: Add Redirect with the code below.

// redirect to the login page if the user is not logged in
redirect: (state) {
  // 1
  final loginLoc = state.namedLocation(loginRouteName);
  // 2
  final loggingIn = state.subloc == loginLoc;
  // 3
  final createAccountLoc = state.namedLocation(createAccountRouteName);
  final creatingAccount = state.subloc == createAccountLoc;
  // 4
  final loggedIn = loginState.loggedIn;
  final rootLoc = state.namedLocation(rootRouteName);

  // 5
  if (!loggedIn && !loggingIn && !creatingAccount) return loginLoc;
  if (loggedIn && (loggingIn || creatingAccount)) return rootLoc;
  return null;
},

In the code above, state.location holds the current route. Returning null means you are not redirecting and the system will use the current location.

Here are some things the code above manages:

  1. Gets the login location.
  2. Checks if the user is going to the login location.
  3. Gets the create account location and checks whether the user is going to the create account location.
  4. Checks if the user is logged in.
  5. Verifies that the current location is neither the Login screen nor the Create Account screen, and the user isn’t logged in. Then, goes to the Login screen.
  6. If the current location is either the Login or the Create Account screens and the user is logged in, goes to the Home screen.
  7. Returns null to say you are not redirecting.

To see the working of the redirects, you need to add the Home routes.

Adding Home Routes

Next, you add the Home screen route and some of its subroutes. Replace // TODO: Add Home route and children in routes.dart with the code below:

GoRoute(
    name: homeRouteName,
    // 1
    path: '/home/:tab(shop|cart|profile)',
    pageBuilder: (context, state) {
      // 2
      final tab = state.params['tab']!;
      return MaterialPage<void>(
          key: state.pageKey,
          // 3
          child: HomeScreen(tab: tab),
      );
      },
      routes: [
        GoRoute(
            name: subDetailsRouteName,
            // 4
            path: 'details/:item',
            pageBuilder: (context, state) => MaterialPage<void>(
              key: state.pageKey,
              // 5
            child: Details(description: state.params['item']!),
            ),
        ),
        GoRoute(
            name: profilePersonalRouteName,
            path: 'personal',
            pageBuilder: (context, state) => MaterialPage<void>(
              key: state.pageKey,
              child: const PersonalInfo(),
            ),
        ),
        GoRoute(
            name: profilePaymentRouteName,
            path: 'payment',
            pageBuilder: (context, state) => MaterialPage<void>(
              key: state.pageKey,
              child: const Payment(),
            ),
        ),
        GoRoute(
            name: profileSigninInfoRouteName,
            path: 'signin-info',
            pageBuilder: (context, state) => MaterialPage<void>(
              key: state.pageKey,
              child: const SigninInfo(),
            ),
        ),
        GoRoute(
            name: profileMoreInfoRouteName,
            path: 'more-info',
            pageBuilder: (context, state) => MaterialPage<void>(
              key: state.pageKey,
              child: const MoreInfo(),
            ),
        ),
      ],
  ),
    // TODO: Add Other routes

In the code above, you define the main routes. The home route uses parameters. Take a look at the path: /home/:tab(shop|cart|profile). This consists of the path home and a tab parameter that will be either shop, cart or profile. These are the three bottom buttons on the Home screen.

Here are some things that the code above handles:

  1. Defines your tab based home screen path.
  2. Gets the tab parameter.
  3. Passes the tab parameter to HomeScreen.
  4. Defines the details path that requires an item.
  5. Passes the item to Details as a description.

Find // TODO: Change to Home Route and replace it with the code below:

state.namedLocation(homeRouteName, params: {'tab': 'shop'}),

Hot restart and try tapping on the Login button on the Login screen or the Create Account button on the Create Account screen. It will take you to the Home screen.

When you’re on the Home screen, hot restart again. You’ll see that the app directly opened on the Home screen instead of the Login screen. This is the result of using redirects.

Adding More Routes

Now, add the rest of the routes. Replace // TODO: Add Other routes in routes.dart with the code below:

// forwarding routes to remove the need to put the 'tab' param in the code
// 1
GoRoute(
  path: '/shop',
  redirect: (state) =>
      state.namedLocation(homeRouteName, params: {'tab': 'shop'}),
),
GoRoute(
  path: '/cart',
  redirect: (state) =>
      state.namedLocation(homeRouteName, params: {'tab': 'cart'}),
),
GoRoute(
  path: '/profile',
  redirect: (state) =>
      state.namedLocation(homeRouteName, params: {'tab': 'profile'}),
),
GoRoute(
  name: detailsRouteName,
  // 2
  path: '/details-redirector/:item',
  // 3
  redirect: (state) => state.namedLocation(
    subDetailsRouteName,
    params: {'tab': 'shop', 'item': state.params['item']!},
  ),
),
GoRoute(
  name: personalRouteName,
  path: '/profile-personal',
  redirect: (state) => state.namedLocation(
    profilePersonalRouteName,
    // 4
    params: {'tab': 'profile'},
  ),
),
GoRoute(
  name: paymentRouteName,
  path: '/profile-payment',
  redirect: (state) => state.namedLocation(
    profilePaymentRouteName,
    params: {'tab': 'profile'},
  ),
),
GoRoute(
  name: signinInfoRouteName,
  path: '/profile-signin-info',
  redirect: (state) => state.namedLocation(
    profileSigninInfoRouteName,
    params: {'tab': 'profile'},
  ),
),
GoRoute(
  name: moreInfoRouteName,
  path: '/profile-more-info',
  redirect: (state) => state.namedLocation(
    profileMoreInfoRouteName,
    params: {'tab': 'profile'},
  ),
),

Here’s what’s happening in the code above:

  1. You set up redirects for your tabs.
  2. You define another route for details that take an item.
  3. You redirect to the shopping detail but pass another parameter to show the shopping tab.
  4. You redirect to the personal page but pass another parameter to highlight the profile tab.

Next, you add the route to Details page.

Implementing the Details Route

Open shopping.dart in the ui directory. In the import section, add:

import 'package:go_router/go_router.dart';

Find and replace // TODO: Add Push Details Route with:

context.goNamed(detailsRouteName, params: {'item': value});

The code above starts the details page and passes in the description of the shopping item.

Now, hot restart. The Shopping page shows a generated list of items.

Shopping screen

When you tap an item, you see the details page:

Detail page

Next, you’ll handle the navigation for when the user taps the Add to Cart button.

Updating the Details Page

Open details.dart in the ui directory. In the import section, add:

import 'package:go_router/go_router.dart';
import '../constants.dart';

Find and replace // TODO: Add Root Route with:

context.goNamed(rootRouteName);

Now, hot reload and navigate to the detail page by tapping on a shopping item. Try tapping the Add to Cart button and then go to the Cart screen to make sure it’s there.

Cart page

Tapping Add to Cart adds an item to the cart and returns to the Shopping screen. When you go to the Cart tab, it will show a list of the items you’ve added.

Using Paths

Open home_screen.dart in the ui directory. In the import section and add:

import 'package:go_router/go_router.dart';

Then, find and replace // TODO: Add Switch with:

switch (index) {
  case 0:
    context.go('/shop');
    break;
  case 1:
    context.go('/cart');
    break;
  case 2:
    context.go('/profile');
    break;
}

In the code above, you’re using paths instead of named routes. This is another way of using go_router. This makes sure the path is correct when switching tabs and is important in web pages that show the path.

Note: Try running the app on a browser to see the URL change when you tap different tabs.

Routing in Profile Page

The Profile screen shows a list of additional pages the user can go to:

Profile page

Open profile.dart in the ui directory. In the import section, add:

import 'package:go_router/go_router.dart';

Find and replace // TODO: Add Personal Page Route with:

context.goNamed(personalRouteName);

The code above will take you to the Personal Info screen. Hot reload and tap Personal Info from the Profile screen, and you’ll see this:

Personal page

Next you’ll take care of the Payment screen. Search for // TODO: Add Payment Route and replace it with:

context.goNamed(paymentRouteName);

This will take you to the Payment screen. Hot reload and tap Payment from the Profile screen, and you’ll see this screen:

Payment page

Now, find and replace // TODO: Add Signin Info Route with:

context.goNamed(signinInfoRouteName);

The code above will take you to the Sign In Info page. Hot reload and tap Sign In Info from the Profile screen to see this:

Sign In page

Finally, search for // TODO: Add More Info Route and replace it with:

context.goNamed(moreInfoRouteName);

This will take you to the More Info screen:

more info page

Now, hot restart and make sure that when you tap all of the buttons on the Profile screen, you can get to those screens. Try tapping the back arrow or the back button on Android. It will take you back to the previous screen. GoRouter handles this because it’s a sub-route of the Profile screen.

From the Sign In link on the Profile screen, tap the Sign out button and see what happens. You’ll be taken back to the Login screen. This is starting to look like a real app!

Congratulations! That was a lot of code, but it will help you whenever you need to implement your navigation system. As you can see, the router code took over 100 lines — with a lot of duplicate code. Much easier than writing your own RouterDelegate and RouterInformationParser!

So, that was Android or iOS, but there’s more. Flutter also works with Mac, Windows and the web. Try running it on the web. You’ll see something like:

Web login screen

Web home screen

If you try running it on the Mac, you’ll see something like:

Mac login screen

Mac home screen

That’s five platforms in an afternoon!

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Check out the following links to learn more about some of the concepts in this tutorial:

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!