A fully annotated Flutter project implementing every concept, built as a working shopping app demo with an interactive concepts hub.
π± Cross-Platform Ready: This project perfectly supports and is optimized for Android, iOS, and Chrome (Web). You can easily learn and explore the concepts by running it directly on any of these platforms!
β‘ Try it now on Android: For ease of setup, you can directly install and test the app on your Android device by downloading the APK here:
π₯ Download Android APK (v1.0.0)
This project teaches you 9 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, pushNamed, pop) |
screens/demo_screens.dart |
| 4 | Path & Query Parameters | screens/product_list_screen.dart, screens/demo_screens.dart |
| 5 | Absolute vs Relative Paths & extra |
screens/demo_screens.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 & Role-Based Guards | config/route_config.dart, screens/demo_screens.dart |
| 9 | Exit Guards (onExit) |
config/route_config.dart, screens/product_purchase_screen.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: ^18.0.1Import 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 (replace) |
context.pushNamed('name') |
Navigate by name (pushes) | Avoiding hardcoded paths (stack) |
context.pop(data) |
Go back (optionally return data) | Back button equivalent, modal returns |
// Navigate by path β replaces current route
context.go('/products');
// Navigate by path β pushes on stack
context.push('/products');
// Navigate by name (replacing)
context.goNamed('product-details', queryParameters: {'id': 'p1'});
// Navigate by name (pushing)
context.pushNamed('product-details', queryParameters: {'id': 'p1'});
// Pop and return data to the previous screen
context.pop('User Selected Yes');π 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
You can also pass raw Dart objects directly to a route using the extra parameter. This data is not serialized into the URL.
context.go('/details', extra: myComplexObject);
// In your route builder:
final obj = state.extra as MyComplexObject;Warning
If a user refreshes the web browser or enters the app via a deep link, the extra object will be null! Only use extra for non-critical, in-memory data passing where URL persistence isn't required.
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: ^18.0.1- π¦ 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".
This project is licensed under the MIT License - see the LICENSE file for details.
This repository is provided for portfolio and evaluation purposes only.
Commercial use, redistribution, modification, or reproduction without written permission is prohibited.
Developed with β€οΈ by Arpit Aswal.