A fully annotated Flutter project implementing every concept, built as a working shopping app demo with an interactive concepts hub.
This project teaches you 8 core GoRouter concepts through working, commented code:
| # | Concept | File(s) |
|---|---|---|
| 1 | Defining Routes (GoRoute) |
config/route_config.dart |
| 2 | Creating the Router (GoRouter) |
config/route_config.dart, main.dart |
| 3 | Navigation Methods (go, push, goNamed, pop) |
screens/demo_screens.dart |
| 4 | Query Parameters | screens/product_list_screen.dart, screens/product_details_screen.dart |
| 5 | Path Parameters | screens/product_details_screen.dart, screens/product_purchase_screen.dart |
| 6 | Sub-Routes (Nested Routes) | config/route_config.dart, screens/demo_screens.dart |
| 7 | ShellRoute (Persistent UI) |
screens/shell_route_demo.dart |
| 8 | Redirection & Exit Guards | config/route_config.dart, screens/demo_screens.dart |
lib/
├── main.dart # App entry point — MaterialApp.router()
├── config/
│ └── route_config.dart # ⭐ Central GoRouter config — all routes defined here
├── models/
│ └── product.dart # Product data model (immutable data class)
├── controller/
│ └── product_controller.dart # Data access layer (in-memory product store)
├── screens/
│ ├── concepts_overview_screen.dart # 🏠 Hub screen — links to all demos
│ ├── product_list_screen.dart # Shopping app: Product grid (uses goNamed + queryParams)
│ ├── product_details_screen.dart # Shopping app: Detail view (Hero animation, path params)
│ ├── product_purchase_screen.dart # Shopping app: Purchase screen (onExit guard)
│ ├── demo_screens.dart # Individual demo screens for each concept
│ └── shell_route_demo.dart # ShellRoute with BottomNavigationBar
└── widgets/
├── single_product.dart # Product card with Hero animation
├── bottom_container.dart # Price + Buy Now bottom sheet widget
├── search_section.dart # Search TextField widget
├── ratings.dart # Star rating row widget
├── color_container.dart # Color swatch widget
└── show_modal.dart # Full-size image dialog widget
- Flutter SDK (3.x+)
- Dart 3.x
- Android Studio / VS Code with Flutter plugin
# Clone and navigate to the project
cd GoRouterNavigation
# Install dependencies
flutter pub get
# Run the app
flutter runAdd go_router to pubspec.yaml:
dependencies:
go_router: ^17.0.0Import in your Dart files:
import 'package:go_router/go_router.dart';Each GoRoute defines one screen with a URL path and a builder function:
GoRoute(
path: '/',
name: 'home', // Optional name for goNamed() navigation
builder: (context, state) => const HomeScreen(),
),Key insight:
statein the builder gives you access to URL parameters, the current URI, and other route metadata.
📁 See: lib/config/route_config.dart
// lib/config/route_config.dart
final GoRouter router = GoRouter(
initialLocation: '/',
debugLogDiagnostics: true, // Logs every navigation to console
routes: <RouteBase>[
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
// ... more routes
],
);In main.dart, pass it to MaterialApp.router():
MaterialApp.router(
routerConfig: router, // ← The key integration point
)Why
MaterialApp.router()? GoRouter uses Flutter's Navigator 2.0 (Router API).MaterialApp.router()is the constructor that accepts aRouterConfigobject (which GoRouter implements).
📁 See: lib/main.dart | lib/config/route_config.dart
GoRouter provides three main navigation methods:
| Method | Behaviour | Use When |
|---|---|---|
context.go('/path') |
Replaces current route | Login → Home (no back) |
context.push('/path') |
Pushes on stack | List → Detail (back works) |
context.goNamed('name') |
Navigate by route name | Avoiding hardcoded paths |
context.pop() |
Go back | Back button equivalent |
// Navigate by path — replaces current route
context.go('/products');
// Navigate by path — pushes on stack
context.push('/products');
// Navigate by name
context.goNamed('product-details', queryParameters: {'id': 'p1'});
// Also valid (interops with GoRouter)
Navigator.of(context).pop();📁 See: lib/screens/demo_screens.dart
Query parameters are optional key-value pairs appended after ? in the URL.
Sending (from ProductListScreen):
context.goNamed(
'product-details',
queryParameters: {'id': product.id},
);
// Resulting URL: /products/product-details?id=p1Receiving (in route_config.dart builder):
GoRoute(
path: 'product-details',
name: 'product-details',
builder: (context, state) {
return ProductDetailsScreen(
productId: state.uri.queryParameters['id'] ?? '',
);
},
),When to use query params? When the data is optional, when you want multiple params without changing the route structure, or when the parameter is a filter/search value.
📁 See: lib/screens/product_list_screen.dart
Path parameters are embedded directly in the URL path. They are required — the route doesn't exist without them.
Defining the route (in route_config.dart):
GoRoute(
path: 'product-purchase/:description', // :description is the dynamic segment
name: 'pay-now',
builder: (context, state) {
return ProductPurchaseScreen(
description: state.pathParameters['description']!, // ← Read path param
productImage: state.uri.queryParameters['img']!, // ← Read query param
);
},
),Navigating with path params:
context.goNamed(
'pay-now',
pathParameters: {'description': product.description},
queryParameters: {'img': product.imageUrl, 'price': '29.99'},
);
// URL: /products/product-details/product-purchase/Great+Watch?img=...&price=29.99When to use path params? When the value is required and defines the route identity (IDs, slugs, usernames).
📁 See: lib/screens/product_details_screen.dart
Sub-routes let you group related routes under a parent. The child path is relative to the parent.
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
routes: [
// Relative path → full URL: /profile/settings
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
),| URL | Screen |
|---|---|
/profile |
ProfileScreen |
/profile/settings |
SettingsScreen |
📁 See: lib/config/route_config.dart | lib/screens/demo_screens.dart
ShellRoute wraps child routes with a persistent UI shell (like a BottomNavigationBar). The shell stays mounted while only the inner content changes.
ShellRoute(
builder: (context, state, child) {
// 'child' = the currently active tab's widget (auto-injected by GoRouter)
return ShellScaffold(child: child);
},
routes: [
GoRoute(path: '/shell/home', builder: (c, s) => const ShellHomeTab()),
GoRoute(path: '/shell/explore', builder: (c, s) => const ShellExploreTab()),
GoRoute(path: '/shell/profile', builder: (c, s) => const ShellProfileTab()),
],
),The ShellScaffold renders the BottomNavigationBar and displays child in its body. When you tap a tab, GoRouter updates child — the scaffold itself is never rebuilt.
📁 See: lib/screens/shell_route_demo.dart
redirect runs before the route builder. Return null to proceed, or return a path string to redirect.
Route-level redirect (protecting a single route):
GoRoute(
path: '/dashboard',
redirect: (context, state) {
return AppState.isLoggedIn ? null : '/login'; // Redirect if not logged in
},
builder: (context, state) => const DashboardScreen(),
),Role-based guard:
GoRoute(
path: '/admin',
redirect: (context, state) {
return AppState.isAdmin ? null : '/not-authorized';
},
builder: ...
),Redirect old URLs:
GoRoute(
path: '/old-home',
redirect: (context, state) => '/', // Always redirect to new home
),📁 See: lib/config/route_config.dart | lib/screens/demo_screens.dart
onExit intercepts navigation away from a screen. Return true to allow leaving, false to cancel.
GoRoute(
path: 'product-purchase/:description',
builder: ...,
onExit: (context, state) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Leave Purchase?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false), // Stay
child: const Text('Stay'),
),
TextButton(
onPressed: () => Navigator.pop(context, true), // Leave
child: const Text('Leave'),
),
],
),
);
return confirmed ?? false; // Default: stay (false)
},
),When to use
onExit? Purchase screens, forms with unsaved data, checkout flows — anywhere you need to confirm before the user leaves.
📁 See: lib/config/route_config.dart
The main demo is a minimalist shopping app that chains all GoRouter concepts:
ProductListScreen (/)
│
│ context.goNamed('product-details', queryParameters: {'id': product.id})
│ URL: /products/product-details?id=p1
▼
ProductDetailsScreen
│
│ context.goNamed('pay-now', pathParameters: {'description': ...},
│ queryParameters: {'img': ..., 'price': ..., 'name': ...})
│ URL: /products/product-details/product-purchase/Great+Watch?img=...
▼
ProductPurchaseScreen
│
│ ← onExit guard fires when user tries to go back
│ Shows confirmation dialog
▼
Back to ProductDetailsScreen (if confirmed)
┌──────────────┐
│ Screens │ ← UI only. No direct data access. Calls controllers.
├──────────────┤
│ Widgets │ ← Reusable UI pieces. Receive data as parameters.
├──────────────┤
│ Controllers │ ← Business logic. Fetches/manages data.
├──────────────┤
│ Models │ ← Pure data classes. No UI, no logic.
├──────────────┤
│ Config │ ← Navigation wiring. All routes in one place.
└──────────────┘
-
Use
MaterialApp.router(routerConfig: router)— not the oldrouteInformationParser+routerDelegatepattern. -
Always use
static const routeNameon each screen class to keep route names consistent and avoid typos. -
Query params for optional data, path params for required data — both patterns shown in the shopping flow.
-
ShellRoutesolves the persistent bottom nav problem elegantly — the shell stays mounted, onlychildchanges. -
redirect()returns null (proceed) or a path string (redirect) — use it for auth, role-based access, and URL migrations. -
onExit()returns aFuture<bool>—falsecancels navigation,trueallows it. -
Sub-routes keep related screens grouped — child paths are relative, automatically prefixed by parent path.
-
debugLogDiagnostics: trueonGoRouterprints all navigation events — turn it off for production.
dependencies:
flutter:
sdk: flutter
go_router: ^17.0.0- 📦 Package: go_router on pub.dev
- 🐙 Source: go_router GitHub
Built as a comprehensive Flutter learning resource — every line is commented to explain the "why", not just the "what".