Flutter: Navigation and Routing
Routes are the map of an app — without them, users are like drivers without a GPS, unable to go anywhere.
📋 Prerequisites: You need to master the following first
- Lesson 6: Material Design & Common Widgets
- Lesson 7: Phase 1 Practice — ShopApp Starter
1. What You Will Learn
- Navigator 1.0: push/pop/pushReplacement and route stack management
- Named routes: routes table / onGenerateRoute / parameter passing
- GoRouter (Navigator 2.0): declarative routing, nested ShellRoute, redirects
- Deep linking and URL strategy
- ShopApp GoRouter routing system: /home /product/:id /cart /checkout
2. A True Story of Lost Pages
(1) Pain Point: Route Stack Chaos
Bob's ShopApp has 10 pages, all navigated via Navigator.push. A user goes from Home → Search → Product → Cart → Checkout, but pressing back returns to the Search page instead of Home. The route stack has 5 pages, requiring 4 back presses to reach Home. Worse, the Web URL doesn't change, and refreshing loses all state.
(2) The GoRouter Solution
GoRouter uses declarative routing where URLs and pages map automatically, and the route stack is determined by configuration rather than push order.
⚙️ Install dependency:
flutter pub add go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Declarative routing: URL ↔ Page mapping
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(path: '/product/:id', builder: (_, state) => DetailPage(id: state.pathParameters['id'])),
GoRoute(path: '/cart', builder: (_, __) => const CartPage()),
],
);
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
(3) Benefit: URL-Driven Navigation + Automatic Back Logic
After using GoRouter, the Web URL updates with page changes and refreshes preserve state; the Android back button follows the route stack automatically, and after checkout, go('/') returns directly to Home.
3. Navigator 1.0 Basics
(1) Route Stack Operations
| Method | Effect | Stack Change |
|---|---|---|
push |
Push a new page | [A] → [A, B] |
pop |
Pop current page | [A, B] → [A] |
pushReplacement |
Replace current page | [A, B] → [A, C] |
pushAndRemoveUntil |
Push and clear to a target page | [A,B,C] → [A, D] |
popUntil |
Pop to a target page | [A,B,C] → [A] |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Basic navigation operations
import 'package:flutter/material.dart';
// Simplified class definitions
class Product {
const Product();
}
class DetailPage extends StatelessWidget {
final Product product;
const DetailPage({super.key, required this.product});
@override
Widget build(BuildContext context) => const Scaffold();
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) => const Scaffold();
}
// Push: navigate to detail page
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage(product: product)),
);
// Pop: go back with result
Navigator.pop(context, result);
// Push replacement: login → home (no back to login)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
// Push and remove until: checkout → home (clear all intermediate)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
(route) => route.isFirst,
);
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
4. Named Routes
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Named route configuration
import 'package:flutter/material.dart';
// Simplified page class definitions
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({super.key, required this.id});
@override Widget build(BuildContext context) => const Scaffold();
}
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/cart': (context) => const CartPage(),
'/checkout': (context) => const CheckoutPage(),
},
onGenerateRoute: (settings) {
// Handle dynamic routes like /product/123
final uri = Uri.parse(settings.name!);
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'product') {
final id = int.parse(uri.pathSegments[1]);
return MaterialPageRoute(builder: (_) => DetailPage(id: id));
}
return null;
},
)
// Navigate with named route
Navigator.pushNamed(context, '/product/42');
Navigator.pushNamed(context, '/cart', arguments: {'source': 'detail'});
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
| Approach | Pros | Cons |
|---|---|---|
| Direct push | Simple, intuitive, type-safe | Routes scattered, hard to maintain |
| Named routes | Centralized management, parameter passing | Parameters not type-safe |
| GoRouter | Declarative, Web-friendly, deep linking | Requires additional dependency |
5. GoRouter Declarative Routing
graph TD
GR[GoRouter] --> R1["/ → HomePage"]
GR --> R2["/product/:id → DetailPage"]
GR --> R3["/cart → CartPage"]
GR --> R4["/checkout → CheckoutPage"]
R1 --> |DeepLink| R2
R2 --> |addToCart| R3
R3 --> |checkout| R4
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
(1) GoRouter Core Concepts
| Concept | Description | Example |
|---|---|---|
GoRoute |
Route declaration | GoRoute(path: '/cart') |
pathParameters |
Path parameters | /product/:id → {'id': '42'} |
queryParams |
Query parameters | ?sort=price → {'sort': 'price'} |
redirect |
Redirect logic | Not logged in → /login |
ShellRoute |
Nested layout | Sub-routes sharing BottomNav |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: ShopApp GoRouter configuration
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Simplified class definitions
class AuthService {
static bool isLoggedIn = false;
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int productId;
const DetailPage({super.key, required this.productId});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
final router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/product/:id',
name: 'product',
builder: (context, state) {
final id = int.parse(state.pathParameters['id']!);
return DetailPage(productId: id);
},
),
GoRoute(
path: '/cart',
name: 'cart',
builder: (context, state) => const CartPage(),
),
GoRoute(
path: '/checkout',
name: 'checkout',
builder: (context, state) => const CheckoutPage(),
),
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
],
);
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
(2) ShellRoute Nested Layout
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: ShellRoute with BottomNav
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Simplified page class definitions
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CategoriesPage extends StatelessWidget {
const CategoriesPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({super.key, required this.id});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
final router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(path: '/categories', builder: (_, __) => const CategoriesPage()),
GoRoute(path: '/cart', builder: (_, __) => const CartPage()),
GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
],
),
GoRoute(path: '/product/:id', builder: (_, state) => DetailPage(
id: int.parse(state.pathParameters['id']!)),
),
GoRoute(path: '/checkout', builder: (_, __) => const CheckoutPage()),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex(context),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
NavigationDestination(icon: Icon(Icons.shopping_cart), label: 'Cart'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
onDestinationSelected: (index) => _onItemTapped(index, context),
),
);
}
int _selectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/categories')) return 1;
if (location.startsWith('/cart')) return 2;
if (location.startsWith('/profile')) return 3;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0: context.go('/');
case 1: context.go('/categories');
case 2: context.go('/cart');
case 3: context.go('/profile');
}
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
6. Navigation Method Comparison
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: go vs push vs replace
import 'package:go_router/go_router.dart';
// go: replace entire stack (Web: URL changes)
context.go('/cart'); // Stack becomes [/cart]
// push: add to stack (Web: URL changes)
context.push('/product/42'); // Stack becomes [/, /product/42]
// replace: replace current route
context.replace('/checkout'); // Stack becomes [/, /checkout]
// go with named route
context.goNamed('product', pathParameters: {'id': '42'});
// go with query parameters
context.go('/products', queryParams: {'sort': 'price', 'order': 'desc'});
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
| Method | Stack Operation | URL Update | Use Case |
|---|---|---|---|
go |
Replaces entire stack | Yes | Primary navigation (Tab switching) |
push |
Pushes new route | Yes | Detail page navigation |
replace |
Replaces current route | Yes | Login → Home |
7. Complete Example: ShopApp Routing System
⚙️ Install dependency:
flutter pub add go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(ShopApp(router: router));
final router = GoRouter(
initialLocation: '/',
routes: [
ShellRoute(
builder: (context, state, child) => MainScaffold(child: child),
routes: [
GoRoute(path: '/', name: 'home', builder: (_, __) => const HomePage()),
GoRoute(path: '/categories', builder: (_, __) => const CategoriesPage()),
GoRoute(path: '/cart', name: 'cart', builder: (_, __) => const CartPage()),
GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
],
),
GoRoute(
path: '/product/:id',
name: 'product',
builder: (_, state) => DetailPage(id: int.parse(state.pathParameters['id']!)),
),
GoRoute(
path: '/checkout',
name: 'checkout',
builder: (_, __) => const CheckoutPage(),
),
],
errorBuilder: (_, state) => Scaffold(
body: Center(child: Text('Page not found: ${state.error}')),
),
);
class ShopApp extends StatelessWidget {
final GoRouter router;
const ShopApp({super.key, required this.router});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'ShopApp',
routerConfig: router,
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
);
}
}
class MainScaffold extends StatelessWidget {
final Widget child;
const MainScaffold({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _calcIndex(context),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
NavigationDestination(icon: Icon(Icons.shopping_cart), label: 'Cart'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
onDestinationSelected: (i) {
final paths = ['/', '/categories', '/cart', '/profile'];
context.go(paths[i]);
},
),
);
}
int _calcIndex(BuildContext context) {
final loc = GoRouterState.of(context).matchedLocation;
if (loc.startsWith('/categories')) return 1;
if (loc.startsWith('/cart')) return 2;
if (loc.startsWith('/profile')) return 3;
return 0;
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
❓ FAQ
redirect callback: check login status, redirect to /login if not logged in.MaterialApp.router instead of MaterialApp.apple-app-site-association file. On Android, configure Asset Links and intent-filter.📖 Summary
- Navigator 1.0 manages the route stack with push/pop, suitable for simple scenarios
- Named routes centralize path management, but parameters aren't type-safe
- GoRouter declarative routing: URL ↔ page auto-mapping, Web-friendly
- ShellRoute implements shared layout (BottomNav) with nested sub-routes
- redirect implements route guards; go/push/replace control stack operations
📝 Exercises
- Basic (⭐): Use GoRouter to configure 3 routes (Home/Cart/Profile) with bottom navigation switching.
- Intermediate (⭐⭐): Add a
/product/:iddetail route, navigate from product cards on the home page, and support path parameter passing. - Challenge (⭐⭐⭐): Implement a complete route guard: redirect unauthenticated users accessing
/profileto/login, and automatically redirect back after login.