All Guides
    Navigation & Routing
    Navigation
    GoRouter
    Routing
    Architecture

    GoRouter Setup Guide

    Published April 2026

    Introduction

    As applications grow and expand, structuring clean navigation paths becomes critical. In early versions of Flutter, navigation was imperatively managed using Navigator 1.0 (invoking Navigator.push and Navigator.pop). While this imperative model works well for simple screen stacks, it is difficult to maintain in complex applications that require deep linking, nested route panels, URL synchronization on the web, or redirect logic.

    To address these limitations, Flutter introduced Navigator 2.0, which uses a declarative design model. However, working with the native Navigator 2.0 API requires writing massive amounts of boilerplate code. To simplify this, the Flutter team maintains GoRouter, a declarative routing package.

    GoRouter synchronizes your app's navigation state with the platform's address bar (on web) and handles deep links on mobile. It uses a declarative, URL-based routing structure, making it simple to pass route parameters, nest sub-routes, and configure authentication guards. In this guide, you will learn to build a production-grade routing configuration with redirect guards and dynamic paths.


    Prerequisites

    Before configuring GoRouter, ensure you have:

    • Read our guide on Understanding Widgets in Flutter.
    • A solid understanding of state management fundamentals.
    • Added the go_router package to your project's pubspec.yaml dependencies:
      dependencies:
        go_router: ^13.0.0
      

    Core Explanation: Declarative Routing Concepts

    GoRouter defines your app's screens as paths in a unified configuration map.

      Path Tree Hierarchy             Redirect / Auth Guard
     ┌──────────────────────┐        ┌──────────────────────┐
     │  / (Home)            │        │  /profile            │
     │  ├── /about          │        │     │                │
     │  ├── /login          │        │     ▼ (Not Logged In)│
     │  └── /course/:slug   │        │  Redirects to /login │
     └──────────────────────┘        └──────────────────────┘
    

    1. Declarative Routing

    Instead of pushing widget instances onto a stack, you navigate by modifying the URL path: e.g., context.go('/about'). GoRouter matches the URL string to your configuration map and rebuilds the screen hierarchy automatically.

    2. Path vs. Query Parameters

    Dynamic routes often need to pass data between screens:

    • Path Parameters: Declared with a colon (e.g. /course/:slug). The parameter value becomes part of the URL path itself (like /course/intro-to-flutter).
    • Query Parameters: Key-value pairs appended to the end of the URL (e.g. /search?q=dart). Ideal for optional inputs like search terms or filters.

    3. Redirect Guards (Authentication Middleware)

    Redirect guards are functions that run before a route transition completes. They allow you to inspect the application state (e.g., checking if a user is logged in) and dynamically redirect the user to a different screen (like /login) if they do not meet the access requirements.


    Practical Example: A Complete App Router

    Let's build a clean, compiler-ready GoRouter configuration featuring dynamic paths, query parameters, an authentication redirect guard, and a custom 404 error page.

    import 'package:flutter/material.dart';
    import 'package:go_router/go_router.dart';
    
    // Mock authentication helper class
    class AuthState {
      static bool isLoggedIn = false; // Toggle to test routing redirects
    }
    
    // ==========================================
    // ROUTER CONFIGURATION
    // ==========================================
    final GoRouter _router = GoRouter(
      initialLocation: '/',
      // Global error page handler (404 Page)
      errorBuilder: (context, state) => const Error404Screen(),
      
      // Global Redirect Guard (Middleware)
      redirect: (BuildContext context, GoRouterState state) {
        final bool loggingIn = state.matchedLocation == '/login';
        
        // If user is not logged in and attempts to access protected routes,
        // redirect them to the login screen.
        if (!AuthState.isLoggedIn) {
          // Allow them to visit public routes like '/' or '/login'
          if (state.matchedLocation == '/' || loggingIn) {
            return null; 
          }
          return '/login'; // Redirect protected routes to /login
        }
    
        // If logged in and attempting to visit login page, send them home
        if (AuthState.isLoggedIn && loggingIn) {
          return '/';
        }
    
        return null; // Return null to proceed with navigation
      },
      
      routes: <RouteBase>[
        // 1. Home Route
        GoRoute(
          path: '/',
          builder: (BuildContext context, GoRouterState state) {
            return const HomeScreen();
          },
          routes: <RouteBase>[
            // 2. Login Route (Sub-route: resolves to /login)
            GoRoute(
              path: 'login',
              builder: (context, state) => const LoginScreen(),
            ),
            // 3. Dynamic Course Route (Sub-route: resolves to /course/:slug)
            GoRoute(
              path: 'course/:slug',
              builder: (context, state) {
                // Extract the path parameter safely
                final slug = state.pathParameters['slug'] ?? 'default-slug';
                return CourseDetailScreen(slug: slug);
              },
            ),
            // 4. Profile Route (Protected route: resolves to /profile)
            GoRoute(
              path: 'profile',
              builder: (context, state) => const ProfileScreen(),
            ),
          ],
        ),
      ],
    );
    
    // ==========================================
    // APP ROOT WRAPPER
    // ==========================================
    class MyAppRouter extends StatelessWidget {
      const MyAppRouter({super.key});
    
      @override
      Widget build(BuildContext context) {
        // We bind the router instance to MaterialApp using the .router constructor
        return MaterialApp.router(
          title: 'GoRouter Example',
          debugShowCheckedModeBanner: false,
          routerConfig: _router,
        );
      }
    }
    
    // ==========================================
    // SCREENS INDEX BUILD
    // ==========================================
    class HomeScreen extends StatelessWidget {
      const HomeScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Home Page')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.go('/course/dart-basics'),
                  child: const Text('View Dart Lesson (/course/dart-basics)'),
                ),
                const SizedBox(height: 12.0),
                ElevatedButton(
                  onPressed: () => context.go('/profile'),
                  child: const Text('View Profile (Protected /profile)'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class LoginScreen extends StatelessWidget {
      const LoginScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Login')),
          body: Center(
            child: ElevatedButton(
              onPressed: () {
                AuthState.isLoggedIn = true; // Log user in
                context.go('/profile'); // Send them to their profile page
              },
              child: const Text('Log In'),
            ),
          ),
        );
      }
    }
    
    class CourseDetailScreen extends StatelessWidget {
      final String slug;
      const CourseDetailScreen({super.key, required this.slug});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Lesson: $slug')),
          body: Center(
            child: ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('Go Home'),
            ),
          ),
        );
      }
    }
    
    class ProfileScreen extends StatelessWidget {
      const ProfileScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Profile')),
          body: Center(
            child: ElevatedButton(
              onPressed: () {
                AuthState.isLoggedIn = false; // Log user out
                context.go('/');
              },
              child: const Text('Log Out'),
            ),
          ),
        );
      }
    }
    
    class Error404Screen extends StatelessWidget {
      const Error404Screen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('404 - Not Found')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('The page you requested does not exist.'),
                const SizedBox(height: 16.0),
                ElevatedButton(
                  onPressed: () => context.go('/'),
                  child: const Text('Go Home'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Code Explanation:

    1. MaterialApp.router: Binds our GoRouter configuration to the application structure, replacing the standard routes or home parameters.
    2. redirect Block: Acts as middleware, running before navigation transitions to check authentication states and route paths.
    3. state.pathParameters['slug']: Safely extracts the dynamic :slug parameter from the URL path.
    4. context.go(): Navigates declaratively by changing the browser's URL path.

    Common Mistakes

    1. Using context.push() instead of context.go()

    context.go() replaces the navigation stack to match the target route's location hierarchy. context.push(), on the other hand, imperatively pushes a new screen onto the stack without updating the parent routing paths. This can break deep links and browser history.

    • The Fix: Use context.go() for primary navigation routes, and limit context.push() to modal dialogs or temporary pop-ups.

    2. Nesting Routes with Leading Slashes

    Declaring sub-routes with leading slashes (e.g. nested route path: '/login') will cause router crash errors.

    • The Fix: Keep nested sub-route paths relative by omitting the leading slash (e.g. path: 'login'). GoRouter automatically combines this with the parent path to resolve to /login.

    3. Missing Error Handlers

    If your app doesn't configure a custom errorBuilder fallback page, GoRouter will display a default, developer-focused red screen when it encounters invalid routes.

    • The Fix: Always configure a custom errorBuilder to display a user-friendly 404 page.

    Best Practices

    • Define Route Paths as Constants: Maintain a unified routes class (Routes.home = '/', Routes.login = '/login') to prevent typos in route names.
    • Keep Redirect Logic Pure: Do not perform heavy database writes or network calculations inside redirect guards. This keeps route transitions fast and responsive.
    • Use Sub-Routes to Model Screen Hierarchies: Define sub-routes under parent routes to model your app's natural hierarchy, ensuring back buttons behave as expected.

    Related Concepts

    Continue mastering navigation configurations by exploring these guides:


    Summary

    • GoRouter is a declarative routing package that synchronizes navigation state with browser URLs.
    • Use path parameters (:slug) and query parameters to pass data between screens.
    • Redirect guards intercept route transitions to implement access control logic (like authentication checks).
    • Use context.go() for URL-based navigation transitions, and context.push() to push temporary modal sheets onto the stack.

    Next Steps

    1. Previous Guide: Using Dio for HTTP Requests
    2. Next Guide: Named vs. Generated Routes
    3. Related Guide: Navigator 2.0 Deep Dive

    Ready to take the next step?

    Start our free Flutter course and build real apps today.

    Start Learning Now