Flutter Navigation with GoRouter: Go vs Push

Source code on GitHub

GoRouter is a popular package for declarative routing in Flutter. It based on the Navigator 2.0 API, and supports deep linking and other common navigation scenarios, all behind an easy to use API.

If you're coming from Navigator 1.0, you'll be familiar with the concept of pushing a route to the navigation stack.

But when using GoRouter, you have two separate options:

  • going to a route
  • pushing a route

This article will explore the difference between the two, so that you can choose the most appropriate one on a case-by-case basis.

Declarative routing with GoRouter

To start off, let's consider a simple route hierarchy made of one top route with two sub-routes:

GoRouter( initialLocation: '/', routes: [ // top-level route GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ // one sub-route GoRoute( path: 'detail', builder: (context, state) => const DetailScreen(), ), // another sub-route GoRoute( path: 'modal', pageBuilder: (context, state) => const MaterialPage( fullscreenDialog: true, child: ModalScreen(), ), ) ], ), ], )

Let's also define 3 pages for our routes:

Home, Detail, and Modal pages
Home, Detail, and Modal pages

Navigating from the top route

Now, suppose that we're in the HomeScreen, which is just a simple page with three buttons, with callbacks defined like this:

// onPressed callback for the first button context.go('/detail'), // onPressed callback for the second button context.push('/detail'), // onPressed callback for the third button context.go('/modal'),

The first and second callback have the same target location (/detail), and as a result they behave in the same way.

That is, in both cases we'll end up with two routes in the navigation stack (home → detail).

From the home page to the detail page
From the home page to the detail page

The difference between Go and Push

From the detail page, we can now navigate to /modal in two different ways:

// onPressed callback for the first button context.go('/modal'), // onPressed callback for the second button context.push('/modal'),
From the detail page to the modal page
From the detail page to the modal page

This time the result is different:

  • If we use go, we end up with the modal page on top of the home page
  • If we use push, we end up with the modal page on top of the detail page
How Go and Push affect the navigation stack
How Go and Push affect the navigation stack

That's because go jumps to the target route (/modal) by discarding the previous route (/detail), since /modal is not a sub-route of /detail:

Route hierarchy with 3 routes: note that modal is not a sub-route of detail
Route hierarchy with 3 routes: note that modal is not a sub-route of detail

Meanwhile, push always adds the target route on top of the existing one, preserving the navigation stack.


This means that once we dismiss the modal page, we navigate back to:

  • the home page, if we used go
  • the detail page, if we used push

Here's a short video showing this behavior:

Routing with go vs push: animated video
Routing with go vs push: animated video

And here's a gist with the full source code:

Note about Breaking Changes in GoRouter 8.0

Starting from GoRouter 8.0, imperatively pushing a route no longer changes the URL on web.

What does this mean in practice?

Well, as far as the navigation stack is concerned, the behaviour is the same:

How Go and Push affect the navigation stack
How Go and Push affect the navigation stack

In other words, if we're currently at /detail and we call context.push('/modal'), the modal page will be added to the stack.

But the web URL will remain the same (e.g. localhost:60710/detail and not localhost:60710/detail/modal), and this can cause some problems - especially if you have a custom redirect callback.

For example, consider this code which redirects the user back to / if the user is logged in and the current route is /signIn:

GoRouter( redirect: (context, state) { final isLoggedIn = authRepository.currentUser != null; final path = state.uri.path; // redirect to '/' if the user is signed in and the path is '/signIn' if (isLoggedIn) { if (path == '/signIn') { return '/'; } } return null; }, ),

If we did originally navigate to the sign in page by calling context.push('/signIn'), the redirect code above will not work, because the path will still point to the previous route (contradicting the principle of least astonishment).

The bottom line?

If you have a custom redirect callback, and you want to use context.push, make sure to add this line inside your main:

// ensure URL changes in the address bar when using push / pushNamed // more info here: https://docs.google.com/document/d/1VCuB85D5kYxPR3qYOjVmw8boAGKb7k62heFyfFHTOvw/edit GoRouter.optionURLReflectsImperativeAPIs = true;

Alternatively, try to not use context.push at all, since this does not play nice with deep links anyway.

Conclusion

Think of go as a way to jump to a new route. This will modify the underlying navigation stack if the new route is not a sub-route of the old one.

On the other hand, push will always push the destination route on top of the existing navigation stack.

If you can, try to not use push and always prefer go instead, since this doesn't require you to set special flags (such as optionURLReflectsImperativeAPIs), and plays nice with deep links on mobile.


For more info about GoRouter, you can check the API docs or head over to the old documentation site (which is a bit outdated but still an excellent resource).

Flutter Foundations Course Now Available

I launched a brand new course that covers GoRouter in great depth, along with other important topics like state management, app architecture, testing, and much more:

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.