Lesson 30 of 51 intermediate

GoRouter

Declarative Routing with Path Parameters and Redirects

Open interactive version (quiz + challenge)

Real-world analogy

If Navigator is like manually flipping pages in a book, GoRouter is like a table of contents with hyperlinks. You define all your routes in one place with URL paths ('/home', '/user/:id', '/settings'), and GoRouter handles the navigation, deep links, browser URLs, and redirects automatically. It is like a GPS for your app — you tell it where to go by address, and it figures out the best route.

What is it?

GoRouter is a declarative routing package for Flutter that maps URL paths to screens. It supports path parameters (/user/:id), query parameters, redirects for auth guards, ShellRoute for persistent layouts like bottom navigation, nested routes, and named routes. It replaces Navigator's imperative push/pop with a URL-based declarative approach that supports deep linking and web URLs.

Real-world relevance

In team_mvp_kit, GoRouter is the sole navigation solution. The router configuration lives in a single file with all routes defined declaratively. A redirect guard checks authentication status on every navigation. ShellRoute wraps the main tabs with a persistent BottomNavigationBar. Path parameters pass IDs to detail screens. This pattern scales cleanly from 5 screens to 50+.

Key points

Code example

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

// Authentication state (simplified)
class AuthService {
  bool isLoggedIn = false;
}

final authService = AuthService();

// Router configuration
final goRouter = GoRouter(
  initialLocation: '/login',
  redirect: (context, state) {
    final loggedIn = authService.isLoggedIn;
    final loggingIn = state.matchedLocation == '/login';

    if (!loggedIn && !loggingIn) return '/login';
    if (loggedIn && loggingIn) return '/home';
    return null;
  },
  routes: [
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    ShellRoute(
      builder: (context, state, child) {
        return MainShell(child: child);
      },
      routes: [
        GoRoute(
          path: '/home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
        GoRoute(
          name: 'userDetail',
          path: '/user/:userId',
          builder: (context, state) {
            final id = state.pathParameters['userId']!;
            return UserDetailScreen(userId: id);
          },
        ),
      ],
    ),
  ],
);

// Main shell with bottom navigation
class MainShell extends StatelessWidget {
  final Widget child;
  const MainShell({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _calculateIndex(context),
        onDestinationSelected: (index) {
          switch (index) {
            case 0:
              context.go('/home');
            case 1:
              context.go('/profile');
          }
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: 'Home',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outlined),
            selectedIcon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }

  int _calculateIndex(BuildContext context) {
    final location = GoRouterState.of(context).matchedLocation;
    if (location.startsWith('/profile')) return 1;
    return 0;
  }
}

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: () {
            authService.isLoggedIn = true;
            context.go('/home');
          },
          child: const Text('Login'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.push('/user/42');
          },
          child: const Text('View User 42'),
        ),
      ),
    );
  }
}

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: () {
            authService.isLoggedIn = false;
            context.go('/login');
          },
          child: const Text('Logout'),
        ),
      ),
    );
  }
}

class UserDetailScreen extends StatelessWidget {
  final String userId;
  const UserDetailScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User $userId')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('User Detail: $userId',
                style: Theme.of(context).textTheme.headlineMedium),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

Line-by-line walkthrough

  1. 1. Import the Material package
  2. 2. Import the go_router package
  3. 3.
  4. 4. Simple AuthService class to track login status
  5. 5. Boolean flag for logged-in state
  6. 6.
  7. 7. Create a global instance of AuthService
  8. 8.
  9. 9. Define the GoRouter configuration
  10. 10. Start at the login screen
  11. 11. Redirect callback runs before every navigation
  12. 12. Check if the user is logged in
  13. 13. Check if they are heading to the login page
  14. 14. If not logged in and not going to login, redirect to login
  15. 15. If logged in but going to login, redirect to home
  16. 16. Otherwise allow the navigation (return null)
  17. 17.
  18. 18.
  19. 19. Define the login route outside the shell
  20. 20.
  21. 21.
  22. 22. ShellRoute wraps child routes with a shared layout
  23. 23. Builder provides the child widget for the current tab
  24. 24. MainShell provides persistent bottom navigation
  25. 25.
  26. 26. Home tab route at /home
  27. 27.
  28. 28.
  29. 29. Profile tab route at /profile
  30. 30.
  31. 31.
  32. 32. User detail route with path parameter
  33. 33. Named route for type-safe navigation
  34. 34. Extract userId from path parameters
  35. 35. Pass userId to the detail screen
  36. 36.
  37. 37.
  38. 38.
  39. 39.
  40. 40.
  41. 41. MainShell widget wraps content with bottom navigation
  42. 42. Accept the child widget that changes per route
  43. 43.
  44. 44. Scaffold with the child as body
  45. 45. NavigationBar for bottom tabs
  46. 46. Calculate selected index from current path
  47. 47. Handle tab selection with context.go
  48. 48. go to /home for first tab
  49. 49. go to /profile for second tab
  50. 50.
  51. 51. Home destination with outlined and filled icons
  52. 52.
  53. 53.
  54. 54.
  55. 55. Profile destination
  56. 56.
  57. 57.
  58. 58.
  59. 59.
  60. 60. Helper method to determine selected tab index
  61. 61. Get the current matched location from GoRouter
  62. 62. If path starts with /profile, select index 1
  63. 63. Default to index 0 (home)
  64. 64.
  65. 65.
  66. 66. LoginScreen with login button
  67. 67. On press: set logged in to true and go to /home
  68. 68. GoRouter redirect will allow this since user is now logged in
  69. 69.
  70. 70.
  71. 71.
  72. 72. HomeScreen with a button to view user detail
  73. 73. context.push adds user detail ON TOP of the home tab
  74. 74. Uses push (not go) so user can press back to return
  75. 75.
  76. 76.
  77. 77.
  78. 78. ProfileScreen with a logout button
  79. 79. On press: set logged in to false and go to /login
  80. 80. GoRouter redirect handles clearing the session
  81. 81.
  82. 82.
  83. 83.
  84. 84. UserDetailScreen receives userId from the route
  85. 85. Display the user ID
  86. 86. context.pop goes back to the previous screen
  87. 87.
  88. 88.

Spot the bug

GoRouter(
  routes: [
    GoRoute(
      path: 'home',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      path: 'user/:id',
      builder: (context, state) {
        return UserScreen(id: state.pathParameters['userId']!);
      },
    ),
  ],
)
Need a hint?
Look carefully at the path format and the parameter name used to extract the value...
Show answer
Two bugs: (1) Paths must start with a slash — 'home' should be '/home' and 'user/:id' should be '/user/:id'. (2) The path parameter is named ':id' but the code extracts 'userId'. Fix: either change the path to '/user/:userId' or change the extraction to state.pathParameters['id']!

Explain like I'm 5

Imagine your app is a building with many rooms. Navigator is like walking through doors one at a time — you push open a door to enter a room, and close it (pop) to go back. GoRouter is like having a building directory in the lobby. Instead of walking through every door, you look up the room number (URL path like '/room/42') and take the elevator directly there. The directory (redirect) can also say 'Sorry, you need a visitor badge (login) before entering that room!' ShellRoute is like the hallway — it stays the same while you walk into different rooms off of it.

Fun fact

GoRouter was originally a community package by Chris Sells, but it was so good that the Flutter team officially adopted it as a first-party package maintained under the flutter.dev organization. It is now the recommended navigation solution for Flutter apps that need URL-based routing, deep linking, or web support!

Hands-on challenge

Set up a complete GoRouter configuration with: a '/login' route, a ShellRoute with bottom navigation containing '/home' and '/profile' tabs, a nested '/user/:userId' route inside the shell, a redirect that forces unauthenticated users to '/login', and named routes for all screens. Navigate between them using context.go, context.push, and context.goNamed.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart