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

1. What You Will Learn


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

DART
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()),
  ],
);
TEXT
> 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

TEXT
> 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

DART
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,
);
TEXT
> 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

TEXT
> 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

DART
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'});
TEXT
> 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

100%
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
TEXT
> 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

TEXT
> 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

DART
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(),
    ),
  ],
);
TEXT
> 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

TEXT
> 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

DART
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');
    }
  }
}
TEXT
> 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

TEXT
> 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

DART
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'});
TEXT
> 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

DART
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;
  }
}
TEXT
> 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

Q Can GoRouter and Navigator 1.0 be mixed?
A Mixing is not recommended. GoRouter internally manages Navigator, and mixing causes route stack inconsistencies.
Q How to hide the BottomNav for sub-routes in a ShellRoute?
A Place routes that don't need BottomNav outside the ShellRoute, such as detail and checkout pages.
Q How does GoRouter implement route guards (auth)?
A Use the redirect callback: check login status, redirect to /login if not logged in.
Q Web page state lost on refresh?
A GoRouter supports URL state restoration by default. Make sure you use MaterialApp.router instead of MaterialApp.
Q What's the difference between push and go?
A push stacks a new route on top; go replaces the entire stack. Use go for tab navigation and push for detail page navigation.
Q How to configure deep linking on iOS?
A You need to configure Associated Domains and an apple-app-site-association file. On Android, configure Asset Links and intent-filter.

📖 Summary


📝 Exercises

  1. Basic (⭐): Use GoRouter to configure 3 routes (Home/Cart/Profile) with bottom navigation switching.
  2. Intermediate (⭐⭐): Add a /product/:id detail route, navigate from product cards on the home page, and support path parameter passing.
  3. Challenge (⭐⭐⭐): Implement a complete route guard: redirect unauthenticated users accessing /profile to /login, and automatically redirect back after login.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏