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:
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).
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'),
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
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
:
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:
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:
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: