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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter Navigator 2.0: Using go_router
25 mins
- Getting Started
- Sneaking a Peek
- Introducing go_router
- Examining GoRouter Components
- Implementing Router
- Adding Routes
- Updating Main
- Implementing Routes
- Using Redirects
- Adding Home Routes
- Adding More Routes
- Implementing the Details Route
- Updating the Details Page
- Using Paths
- Routing in Profile Page
- Where to Go From Here?
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.
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.
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:
Sneaking a Peek
The app starts with the Login page, which looks like this:
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:
-
LoginState
stores the user’s logged in state. - You create a variable that holds a
GoRouter
instance. - Then, you set the router to listen for changes to the
loginState
. - Show debugging logs.
- Choose the path url strategy (can use hash ‘#’).
- 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.
debugLogDiagnostics
flag before shipping your app.This defines the basics for using GoRouter
. Now, you’ll need three things:
- A list of all the routes to your screens.
- Error Handler. If a route comes in (maybe from a deep link), handle an invalid route.
- 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:
- Path — This is a string in the form of
/path
. - Name — defined in the
constants.dart
file (optional). - 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
orgoNamed
: Replaces the current page with a new stack of the given page -
push
orpushNamed
: 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.
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:
- Gets the login location.
- Checks if the user is going to the login location.
- Gets the create account location and checks whether the user is going to the create account location.
- Checks if the user is logged in.
- 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.
- If the current location is either the Login or the Create Account screens and the user is logged in, goes to the Home screen.
- 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:
- Defines your tab based home screen path.
- Gets the tab parameter.
- Passes the tab parameter to
HomeScreen
. - Defines the details path that requires an item.
- 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:
- You set up redirects for your tabs.
- You define another route for details that take an item.
- You redirect to the shopping detail but pass another parameter to show the shopping tab.
- 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.
When you tap an item, you see the details 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.
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.
Routing in Profile Page
The Profile screen shows a list of additional pages the user can go to:
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:
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:
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:
Finally, search for // TODO: Add More Info Route
and replace it with:
context.goNamed(moreInfoRouteName);
This will take you to the More Info screen:
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:
If you try running it on the Mac, you’ll see something like:
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!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more