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".