Navigation & Routing

Introduction to GoRouter

15 min Lesson 9 of 14

Introduction to GoRouter

As Flutter apps grow in complexity, the built-in Navigator API can become difficult to manage. GoRouter is an official, declarative routing package from the Flutter team that introduces URL-based navigation, deep linking, and a clean route tree structure. Instead of pushing and popping routes imperatively, you declare your entire route hierarchy upfront and navigate using URL-like paths.

Note: GoRouter is the recommended routing solution for Flutter apps that require deep linking, web support, or complex nested navigation. It is maintained by the Flutter team and ships as part of the go_router package on pub.dev.

Adding go_router to Your Project

Start by adding the dependency to your pubspec.yaml file:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.0

After saving the file, run flutter pub get in your terminal to fetch the package. Then import it wherever you configure routing:

Import GoRouter

import 'package:go_router/go_router.dart';

Declaring a Route Tree with GoRoute

GoRouter requires you to declare a route tree — a list of GoRoute entries that map URL paths to widgets. Each GoRoute specifies a path and a builder (or pageBuilder) that returns the widget to display. You then pass the GoRouter instance to your MaterialApp.router constructor.

Defining a GoRouter with Multiple Routes

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) {
        return const HomeScreen();
      },
    ),
    GoRoute(
      path: '/profile',
      builder: (BuildContext context, GoRouterState state) {
        return const ProfileScreen();
      },
    ),
    GoRoute(
      path: '/product/:id',
      builder: (BuildContext context, GoRouterState state) {
        final String productId = state.pathParameters['id']!;
        return ProductScreen(productId: productId);
      },
    ),
  ],
);

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'GoRouter Demo',
      routerConfig: _router,
    );
  }
}
Tip: Keep your GoRouter instance as a top-level or provider-scoped variable, not inside a widget's build method. Recreating the router on every rebuild causes navigation state to reset unexpectedly.

Navigating with context.go and context.push

GoRouter adds extension methods directly on BuildContext so you can navigate anywhere in your widget tree without needing a reference to the router object. The two most important methods are:

  • context.go(path) — Replaces the entire navigation stack with the new route. The user cannot press the back button to return. Use this for top-level navigation like switching tabs or going to a home screen after login.
  • context.push(path) — Pushes the new route on top of the current stack, preserving the back button behaviour. Use this for drilling into details: going from a list to a detail screen.

Using context.go and context.push

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // Replaces the stack — no back button to Home
                context.go('/profile');
              },
              child: const Text('Go to Profile'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // Pushes on top of stack — back button returns here
                context.push('/product/42');
              },
              child: const Text('View Product 42'),
            ),
          ],
        ),
      ),
    );
  }
}

Path Parameters and Query Parameters

GoRouter supports both path parameters (colon-prefixed segments like :id) and query parameters (key-value pairs after ?). Access them through the GoRouterState object passed to every builder:

  • state.pathParameters['id'] — extracts a path segment value
  • state.uri.queryParameters['filter'] — extracts a query string value

To pass a query parameter when navigating, simply append it to the path string:

Path and Query Parameters Example

// Navigating with a query parameter
context.go('/profile?tab=settings');

// In the route builder:
GoRoute(
  path: '/profile',
  builder: (BuildContext context, GoRouterState state) {
    final String tab = state.uri.queryParameters['tab'] ?? 'overview';
    return ProfileScreen(initialTab: tab);
  },
),

Replacing Navigator.push with GoRouter

If you are migrating an existing app, replace every Navigator.push(context, MaterialPageRoute(builder: ...)) call with a context.push('/path') call, and every Navigator.pushReplacement(...) call with context.go('/path'). This makes navigation intent explicit and enables deep linking automatically.

Warning: Do not mix Navigator.push and context.go/context.push in the same app unless you fully understand how GoRouter interacts with the underlying Navigator. Mixing them can produce unexpected back-stack behaviour, especially on the web.

Summary

GoRouter brings URL-based, declarative routing to Flutter. You add the go_router package, define a route tree with GoRoute entries in a GoRouter instance, attach it to MaterialApp.router, and navigate using context.go() (replace stack) or context.push() (add to stack). Path and query parameters are declared in the route path string and accessed via GoRouterState. This foundation makes deep linking, nested navigation, and URL synchronisation straightforward in both mobile and web Flutter apps.

Key Takeaway: Prefer context.go() when you want to replace the navigation history (e.g., after login) and context.push() when you want the user to be able to press back (e.g., viewing a detail screen). The route tree declared in GoRouter is the single source of truth for all navigation in your app.