Dart Tutorial

Flutter Lesson 35 of 83 4 min read

Routing with go_router in Flutter

Set up URL-based navigation in Flutter with go_router: routes, path and query parameters, redirects for login, and nested navigation.

On this page

Navigator.push is simple, but it knows nothing about URLs. As an app grows you need:

  • A browser address bar that works on the web.
  • Deep links: opening myapp.com/product/42 straight to that product.
  • Redirecting to login when the user is signed out.
  • One place where every screen is listed.

The go_router package, maintained by the Flutter team, provides all of this.

flutter pub add go_router

Defining routes #

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

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'product/:id', // becomes /product/:id
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return ProductPage(id: id);
          },
        ),
      ],
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsPage(),
    ),
  ],
);

void main() => runApp(MaterialApp.router(routerConfig: router));

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () => context.push('/settings'),
          ),
        ],
      ),
      body: ListView(
        children: [
          for (final id in ['1', '2', '3'])
            ListTile(
              title: Text('Product $id'),
              onTap: () => context.go('/product/$id'),
            ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product $id')),
      body: Center(child: Text('Details for product $id')),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: const Text('Settings')));
  }
}

Use MaterialApp.router with routerConfig. There is no home.

go or push? #

CallEffect
context.go('/product/7')Jump to that location. The stack is rebuilt from the route tree, so back goes to the parent route
context.push('/settings')Put the page on top of whatever is showing. Back returns to it
context.pop()Go back. Can carry a result: context.pop(value)
context.replace('/home')Swap the current page

Use go for moving around the app’s structure and push for a temporary page.

Parameters #

// Path parameter:  /product/42
GoRoute(
  path: '/product/:id',
  builder: (context, state) => ProductPage(id: state.pathParameters['id']!),
)

// Query parameters:  /search?q=shoes&sort=price
GoRoute(
  path: '/search',
  builder: (context, state) {
    final query = state.uri.queryParameters['q'] ?? '';
    final sort = state.uri.queryParameters['sort'] ?? 'relevance';
    return SearchPage(query: query, sort: sort);
  },
)

// Build the address safely:
context.go(Uri(path: '/search', queryParameters: {'q': 'shoes'}).toString());

Values from a URL are always strings. Convert and validate them, since a user can type anything in the address bar.

Named routes #

Names protect you from typos in paths and from breaking links when a path changes.

GoRoute(
  name: 'product',
  path: '/product/:id',
  builder: (context, state) => ProductPage(id: state.pathParameters['id']!),
)

context.goNamed('product', pathParameters: {'id': '42'});

Redirects: protecting pages #

redirect runs before every navigation. Return a new location, or null to continue.

final router = GoRouter(
  refreshListenable: authState, // a ChangeNotifier: re-check when login state changes
  redirect: (context, state) {
    final loggedIn = authState.isLoggedIn;
    final goingToLogin = state.matchedLocation == '/login';

    if (!loggedIn && !goingToLogin) return '/login';
    if (loggedIn && goingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/', builder: (context, state) => const HomePage()),
    GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
  ],
);

When the user logs in or out, authState notifies and the router applies the redirect again automatically.

A page for unknown addresses #

GoRouter(
  errorBuilder: (context, state) => Scaffold(
    body: Center(child: Text('Nothing found at ${state.uri}')),
  ),
  routes: const [], // your routes here
)

Bottom navigation with its own stack per tab #

StatefulShellRoute keeps the navigation bar on screen and gives each tab its own history.

StatefulShellRoute.indexedStack(
  builder: (context, state, navigationShell) => Scaffold(
    body: navigationShell,
    bottomNavigationBar: NavigationBar(
      selectedIndex: navigationShell.currentIndex,
      onDestinationSelected: navigationShell.goBranch,
      destinations: const [
        NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
        NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
      ],
    ),
  ),
  branches: [
    StatefulShellBranch(routes: [
      GoRoute(path: '/', builder: (context, state) => const HomePage()),
    ]),
    StatefulShellBranch(routes: [
      GoRoute(path: '/profile', builder: (context, state) => const ProfilePage()),
    ]),
  ],
)

Passing objects #

context.push('/edit', extra: note) passes any object, read with state.extra as Note. It is convenient, but the object is lost if the page is opened from a link or refreshed in a browser. Prefer passing an id in the path.

Try it yourself #

Convert a two-screen app to go_router. Add a /login route and a simple ChangeNotifier with an isLoggedIn flag. Redirect to login when signed out, and back to home after signing in.

Practise in the playground Updated by Santosh Adhikari