The declarative navigation is to describe which route should be active.
So far we’ve used Imperative Navigation (Navigator.push). That is, we manually “give orders”: “Put this screen on top”, “Remove this screen”.
This works wonderfully for simple apps. But what if you’re building a Web version of your App? If the user types myapp.com/products/15 in the browser’s address bar, with Navigator.push you don’t have an easy way to know you need to go directly to that screen.
To solve that problem we use Declarative Navigation. In Flutter, one of the most common tools for this is GoRouter.
The Problem with Imperative Navigation
Imagine you have a push notification. When the user taps it, you want to take them to the “Order Detail” screen.
If you use Navigator.push, you have to manually rebuild the path:
- Go to Home.
- Push the order list.
- Push the detail.
It’s error-prone and hard to maintain. Additionally, it doesn’t sync with the URL on the web.
Named Routes
Flutter includes a native named routes system, although its documentation recommends other solutions for most applications with advanced navigation requirements.
It consists of defining a “map” of routes in the MaterialApp:
// Valid approach, though with limitations
MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);And then navigate using names:
Navigator.pushNamed(context, '/details');The problem? Passing arguments is a headache (you have to extract them from ModalRoute) and it doesn’t support “Deep Links” well.
The go_router Package
To solve these scenarios, the official Flutter ecosystem offers the go_router package.
GoRouter uses URL-based navigation, even on mobile.
- If you want to go to the home ->
/ - If you want to go to settings ->
/settings - If you want to view user 42 ->
/users/42
How does it work conceptually?
Instead of manually stacking screens, you define a route tree. When the URL changes (either because you tapped a button or typed in the browser), GoRouter decides which screens should be on the stack to match that URL.
It is Declarative: “I want to be in this state”, instead of “Do this, then this”.
A Look at the Configuration
To use it, you need to add go_router to your pubspec.yaml.
import 'package:go_router/go_router.dart';
// 1. Define the configuration
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
// Nested route: /details
GoRoute(
path: 'details',
builder: (context, state) => DetailsScreen(),
),
],
),
],
);
void main() {
runApp(MaterialApp.router(
// 2. Connect GoRouter to the App
routerConfig: _router,
));
}Navigating with GoRouter
The great thing is that moving around and passing parameters is very clean:
// Go to a page (Replaces the stack if necessary)
context.go('/details');
// Pass parameters in the URL (e.g., /users/123)
context.go('/users/$userId');Features of go_router
Here are some of the features that make it useful for applications with complex navigation:
- Deep Linking: If someone sends you a WhatsApp link
myapp://offer/50, GoRouter opens your App and navigates exactly to that screen automatically. - Redirects (Guards): You can configure security logic. “If the user tries to go to
/profilebut isn’t logged in, automatically redirect them to/login”. - Web Support: The browser’s address bar changes as you navigate through the App.
- Route Parameters: Extracting the
idfrom/product/:idis trivial (state.pathParameters['id']).
Which Approach to Use
I know GoRouter can seem complex to configure at first compared to a simple Navigator.push. Here is my recommendation for the course:
- Are you learning? Keep using
Navigator.push(Imperative). It’s easier to understand at the beginning and sufficient for small apps. - Do you need web, deep links, or redirects? Learn
go_router. It simplifies syncing with URLs and direct access to routes.