Flutter: 导航与路由
路由是应用的地图——没有它,用户就像没有导航仪的司机,哪里都去不了。
📋 前置知识:需要先掌握以下内容
1. 你将学到
- Navigator 1.0:push/pop/pushReplacement 与路由栈管理
- 命名路由:routes 表 / onGenerateRoute / 传参方式
- GoRouter(Navigator 2.0):声明式路由、嵌套路由 ShellRoute、重定向
- 深度链接 Deep Linking 与 URL 策略
- ShopApp GoRouter 路由体系:/home /product/:id /cart /checkout
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 声明式路由
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 怎么实现路由守卫(鉴权)?
A 用
redirect 回调:检查登录状态,未登录重定向到 /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。📖 小节
- Navigator 1.0 用 push/pop 管理路由栈,适合简单场景
- 命名路由集中管理路径,但参数不够类型安全
- GoRouter 声明式路由:URL ↔ 页面自动映射,Web 友好
- ShellRoute 实现共享布局(BottomNav),子路由嵌套其中
- redirect 实现路由守卫,go/push/replace 控制栈操作
📝 作业
- 基础题(难度⭐):用 GoRouter 配置 3 个路由(首页/购物车/个人中心),实现底部导航切换。
- 进阶题(难度⭐⭐):添加
/product/:id详情页路由,从首页商品卡片点击跳转,支持路径参数传递。 - 挑战题(难度⭐⭐⭐):实现完整的路由守卫:未登录用户访问
/profile时重定向到/login,登录后自动跳回。