Flutter: 导航与路由

路由是应用的地图——没有它,用户就像没有导航仪的司机,哪里都去不了。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个页面迷路的真实故事

(1) 痛点:路由栈混乱

Bob 的 ShopApp 有 10 个页面,用 Navigator.push 跳转。用户从首页 → 搜索 → 商品 → 购物车 → 结账后,按返回键竟然回到了搜索页,而不是首页。路由栈里堆了 5 个页面,按 4 次返回才能回到首页。更糟的是,Web 端 URL 不变,刷新就丢失状态。

(2) GoRouter 的解法

GoRouter 采用声明式路由,URL 与页面自动映射,路由栈由配置决定而非 push 顺序。

⚙️ 安装依赖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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:URL 驱动导航 + 自动返回逻辑

Bob 用 GoRouter 后,Web 端 URL 随页面变化,刷新不丢状态;Android 返回键自动按路由栈回退,结账后用 go('/') 直接回到首页。


3. Navigator 1.0 基础

(1) 路由栈操作

方法 效果 栈变化
push 压入新页面 [A] → [A, B]
pop 弹出当前页 [A, B] → [A]
pushReplacement 替换当前页 [A, B] → [A, C]
pushAndRemoveUntil 压入并清空到指定页 [A,B,C] → [A, D]
popUntil 弹出到指定页 [A,B,C] → [A]

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:基础导航操作

DART
import 'package:flutter/material.dart';

// 简化类定义
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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

4. 命名路由

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:命名路由配置

DART
import 'package:flutter/material.dart';

// 简化页面类定义
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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
方式 优点 缺点
直接 push 简单直观、类型安全 路由分散、难维护
命名路由 集中管理、可传参 参数不类型安全
GoRouter 声明式、Web 友好、深度链接 需要额外依赖

5. GoRouter 声明式路由

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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(1) GoRouter 核心概念

概念 说明 示例
GoRoute 路由声明 GoRoute(path: '/cart')
pathParameters 路径参数 /product/:id{'id': '42'}
queryParams 查询参数 ?sort=price{'sort': 'price'}
redirect 重定向逻辑 未登录 → /login
ShellRoute 嵌套布局 共享 BottomNav 的子路由

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:ShopApp GoRouter 配置

DART
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

// 简化类定义
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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(2) ShellRoute 嵌套布局

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:带 BottomNav 的 ShellRoute

DART
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

// 简化页面类定义
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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. 导航方法对比

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

: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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
方法 栈操作 URL 更新 适用场景
go 替换整个栈 主导航(Tab 切换)
push 压入新路由 详情页跳转
replace 替换当前路由 登录→首页

7. 完整示例:ShopApp 路由体系

⚙️ 安装依赖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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q GoRouter 和 Navigator 1.0 可以混用吗?
A 不建议混用。GoRouter 内部管理 Navigator,混用会导致路由栈不一致。
Q ShellRoute 中的子路由怎么隐藏 BottomNav?
A 把不需要 BottomNav 的路由放在 ShellRoute 外面,如详情页和结账页。
Q GoRouter 怎么实现路由守卫(鉴权)?
Aredirect 回调:检查登录状态,未登录重定向到 /login
Q Web 端刷新页面状态丢失?
A GoRouter 默认支持 URL 状态恢复。确保使用 MaterialApp.router 而非 MaterialApp
Q push 和 go 什么区别?
A push 在当前栈上叠加新路由,go 替换整个栈。Tab 导航用 go,详情跳转用 push。
Q 深度链接在 iOS 上怎么配置?
A 需要配置 Associated Domains 和 apple-app-site-association 文件。Android 配置 Asset Links 和 intent-filter。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 GoRouter 配置 3 个路由(首页/购物车/个人中心),实现底部导航切换。
  2. 进阶题(难度⭐⭐):添加 /product/:id 详情页路由,从首页商品卡片点击跳转,支持路径参数传递。
  3. 挑战题(难度⭐⭐⭐):实现完整的路由守卫:未登录用户访问 /profile 时重定向到 /login,登录后自动跳回。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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