2.4 路由管理
📚 核心知识点
- 路由的概念
- Navigator基本使用
- 路由传参和返回值
- 命名路由
- 路由生成钩子
💡 核心概念
什么是路由?
路由(Route) 在移动开发中通常指页面(Page)。
- Android中:一个Activity
- iOS中:一个ViewController
- Web中:一个Page
- Flutter中:一个Widget
路由管理 = 页面导航管理
Navigator - 路由管理器
Navigator维护一个路由栈:
┌─────────────────┐
│ 第三个页面 │ ← 栈顶(当前显示)
├─────────────────┤
│ 第二个页面 │
├─────────────────┤
│ 首页 │ ← 栈底
└─────────────────┘
基本操作:
-
push - 入栈(打开新页面)
-
pop - 出栈(返回上一页)
路由栈操作流程
flowchart TB
subgraph "路由栈状态变化"
A1["初始:<br/>[首页]"]
A2["push 第二页<br/>[首页, 第二页]"]
A3["push 第三页<br/>[首页, 第二页, 第三页]"]
A4["pop 返回<br/>[首页, 第二页]"]
A5["pushReplacement 登录页<br/>[首页, 登录页]"]
end
A1 --> |Navigator.push| A2
A2 --> |Navigator.push| A3
A3 --> |Navigator.pop| A4
A4 --> |Navigator.pushReplacement| A5
style A1 fill:#E3F2FD
style A2 fill:#BBDEFB
style A3 fill:#90CAF9
style A4 fill:#BBDEFB
style A5 fill:#FFF9C4
🎯 方式1:基本路由跳转
打开新页面
// 使用 MaterialPageRoute 打开新页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
);
// 也可以使用其他路由类型(iOS风格)
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => SecondPage(),
),
);
返回上一页
// 方法1:手动调用
Navigator.pop(context);
// 方法2:点击AppBar自动返回按钮
// Flutter会自动在AppBar添加返回按钮
完整示例
// 首页
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 打开新页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
);
},
child: Text('打开第二页'),
),
),
);
}
}
// 第二页
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('第二页')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context); // 返回
},
child: Text('返回'),
),
),
);
}
}
🎯 方式2:路由传参和返回值
数据流向图
sequenceDiagram
participant A as 页面A
participant N as Navigator
participant B as 页面B
Note over A: 用户触发跳转
A->>N: push(页面B, 参数: "Hello")
N->>B: 创建页面B<br/>传入参数 "Hello"
Note over B: 显示页面B<br/>接收到参数 "Hello"
Note over B: 用户操作完成
B->>N: pop(返回值: "Success")
N->>A: 返回到页面A<br/>携带返回值 "Success"
Note over A: 接收返回值<br/>更新UI
打开页面时传参
// 定义接收参数的页面
class DetailPage extends StatelessWidget {
final String title;
final int id;
const DetailPage({required this.title, required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('ID: $id'),
),
);
}
}
// 跳转并传参
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
title: '商品详情',
id: 123,
),
),
);
获取返回值
// 返回时传递数据
Navigator.pop(context, '返回的数据');
// 接收返回值(使用async/await)
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => SelectPage()),
);
if (result != null) {
print('用户选择了:$result');
}
完整示例:选择器
// 首页
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _selected = '未选择';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前选择:$_selected'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
// 等待返回值
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => SelectPage(),
),
);
if (result != null) {
setState(() {
_selected = result;
});
}
},
child: Text('去选择'),
),
],
),
),
);
}
}
// 选择页
class SelectPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('请选择')),
body: ListView(
children: [
ListTile(
title: Text('选项A'),
onTap: () => Navigator.pop(context, 'A'),
),
ListTile(
title: Text('选项B'),
onTap: () => Navigator.pop(context, 'B'),
),
ListTile(
title: Text('选项C'),
onTap: () => Navigator.pop(context, 'C'),
),
],
),
);
}
}
🎯 方式3:命名路由
为什么用命名路由?
✅ 优点:
- 语义化更明确(
/home, /detail)
- 代码更好维护(统一管理)
- 可以做全局拦截(权限控制)
注册路由表
在MaterialApp中注册:
MaterialApp(
title: 'My App',
// 设置首页
initialRoute: '/',
// 注册路由表
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
)
使用命名路由
// 打开页面
Navigator.pushNamed(context, '/detail');
// 替换当前页面
Navigator.pushReplacementNamed(context, '/home');
// 清空栈并打开新页面
Navigator.pushNamedAndRemoveUntil(
context,
'/home',
(route) => false, // 移除所有页面
);
命名路由传参
有两种传参方式:
方法1:通过 arguments 传参(推荐)
优点: 灵活,适合需要传递多个参数的场景
// 1. 注册路由时获取参数
routes: {
'/detail': (context) {
final args = ModalRoute.of(context)?.settings.arguments as Map?;
return DetailPage(
title: args?['title'] ?? '',
id: args?['id'] ?? 0,
);
},
}
// 2. 跳转时传参
Navigator.pushNamed(
context,
'/detail',
arguments: {
'title': '商品详情',
'id': 123,
},
);
方法2:通过 onGenerateRoute 统一处理(推荐用于复杂项目)
优点: 统一管理,可以做参数校验、类型转换、权限检查
// 1. 不注册 routes,使用 onGenerateRoute
MaterialApp(
onGenerateRoute: (settings) {
// 根据路由名称判断
if (settings.name == '/detail') {
final args = settings.arguments as Map?;
return MaterialPageRoute(
builder: (context) => DetailPage(
title: args?['title'] ?? '',
id: args?['id'] ?? 0,
),
);
}
if (settings.name == '/user') {
final userId = settings.arguments as int?;
// 可以在这里做权限检查
if (userId == null) {
return MaterialPageRoute(
builder: (context) => ErrorPage(message: '用户ID不能为空'),
);
}
return MaterialPageRoute(
builder: (context) => UserPage(userId: userId),
);
}
return null; // 未找到路由
},
)
// 2. 跳转时传参(和方法1一样)
Navigator.pushNamed(context, '/detail', arguments: {'title': '商品详情', 'id': 123});
🎯 方式4:路由生成钩子
onGenerateRoute - 统一权限控制
onGenerateRoute会在打开命名路由时调用,可以用来:
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
// 获取路由名称
String? routeName = settings.name;
// 需要登录的页面列表
List<String> authRoutes = ['/profile', '/cart', '/orders'];
// 检查是否需要登录
if (authRoutes.contains(routeName)) {
// 检查登录状态
bool isLoggedIn = checkLoginStatus();
if (!isLoggedIn) {
// 未登录,跳转到登录页
return MaterialPageRoute(
builder: (context) => LoginPage(
redirectTo: routeName, // 记录原本要去的页面
),
);
}
}
// 其他情况返回null,让Flutter使用routes表
return null;
},
routes: {
'/home': (context) => HomePage(),
'/profile': (context) => ProfilePage(),
'/cart': (context) => CartPage(),
},
)
完整示例:登录拦截
class MyApp extends StatelessWidget {
// 模拟登录状态
static bool isLoggedIn = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/home': (context) => HomePage(),
'/profile': (context) => ProfilePage(),
},
onGenerateRoute: (settings) {
// 拦截需要登录的页面
if (settings.name == '/profile' && !isLoggedIn) {
return MaterialPageRoute(
builder: (context) => LoginPage(
onLoginSuccess: () {
// 登录成功后跳转到原页面
Navigator.pushReplacementNamed(context, '/profile');
},
),
);
}
return null;
},
);
}
}
📊 Navigator常用方法
打开页面
| 方法 |
说明 |
栈变化 |
push |
打开新页面 |
[A, B] → [A, B, C] |
pushReplacement |
替换当前页面 |
[A, B] → [A, C] |
pushAndRemoveUntil |
打开页面并移除之前的页面 |
[A, B, C] → [D] |
返回页面
| 方法 |
说明 |
pop |
返回上一页 |
popUntil |
返回到指定页面 |
popAndPushNamed |
返回并打开新页面 |
maybePop |
如果可以返回则返回 |
示例
// 1. 替换当前页面(登录后跳转首页)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
// 2. 清空栈并打开新页面(退出登录)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
(route) => false, // 移除所有页面
);
// 3. 返回到首页
Navigator.popUntil(context, ModalRoute.withName('/'));
// 4. 如果可以返回则返回(否则什么都不做)
Navigator.maybePop(context);
🎯 核心总结
选择哪种方式?
| 场景 |
推荐方式 |
| 简单跳转,无需复用 |
基本路由 |
| 需要传递对象 |
基本路由 + 构造参数 |
| 需要全局管理 |
命名路由 |
| 需要权限控制 |
命名路由 + onGenerateRoute |
最佳实践
✅ 建议统一使用命名路由
原因:
- 语义化更明确
- 代码更好维护
- 可以全局拦截
- 便于实现deep link
路由流程
flowchart TB
A["Navigator.pushNamed"]
B{"routes表中<br/>有这个路由?"}
C["使用routes中<br/>的builder"]
D["调用onGenerateRoute"]
E{"返回值"}
F["使用返回的Route"]
G["调用onUnknownRoute"]
A --> B
B -->|"✅ 有"| C
B -->|"❌ 没有"| D
D --> E
E -->|"Route"| F
E -->|"null"| G
style C fill:#C8E6C9
style F fill:#C8E6C9
style G fill:#FFCDD2
📝 常见问题
Q1: pop时如何判断是否能返回?
A: 使用 Navigator.canPop(context)
if (Navigator.canPop(context)) {
Navigator.pop(context);
} else {
// 已经是栈底,不能再返回
print('已经是第一个页面了');
}
// 或者使用
Navigator.maybePop(context); // 自动判断
Q2: 如何监听返回按钮?
A: 使用 WillPopScope
WillPopScope(
onWillPop: () async {
// 返回true允许返回,false阻止返回
bool shouldPop = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('确认退出?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('确认'),
),
],
),
);
return shouldPop;
},
child: Scaffold(
// ...
),
)
Q3: 命名路由如何传递复杂对象?
A:
// 定义参数类
class DetailPageArgs {
final String title;
final User user;
final List<String> tags;
DetailPageArgs({required this.title, required this.user, required this.tags});
}
// 注册路由
routes: {
'/detail': (context) {
final args = ModalRoute.of(context)!.settings.arguments as DetailPageArgs;
return DetailPage(
title: args.title,
user: args.user,
tags: args.tags,
);
},
}
// 跳转
Navigator.pushNamed(
context,
'/detail',
arguments: DetailPageArgs(
title: 'User Detail',
user: currentUser,
tags: ['tag1', 'tag2'],
),
);
Q4: 如何实现侧滑返回(iOS效果)?
A: 使用 CupertinoPageRoute
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => SecondPage(),
),
);
Q5: onGenerateRoute和routes的区别?
A:
-
routes:静态路由表,简单直接
-
onGenerateRoute:动态路由生成,可以做拦截
执行顺序:
- 先查找
routes 表
- 如果没找到,调用
onGenerateRoute
- 如果还是null,调用
onUnknownRoute
🎓 跟着做练习
练习1:实现一个商品列表和详情页 ⭐⭐
要求:
- 列表页显示商品列表
- 点击商品跳转到详情页
- 详情页接收商品ID和名称
- 详情页有返回按钮
// 商品模型
class Product {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// 列表页
class ProductListPage extends StatelessWidget {
final List<Product> products = [
Product(id: 1, name: 'iPhone', price: 5999),
Product(id: 2, name: 'iPad', price: 3999),
Product(id: 3, name: 'MacBook', price: 9999),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('商品列表')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('¥${product.price}'),
trailing: Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(product: product),
),
);
},
);
},
),
);
}
}
// 详情页
class ProductDetailPage extends StatelessWidget {
final Product product;
const ProductDetailPage({required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ID: ${product.id}', style: TextStyle(fontSize: 20)),
Text('名称: ${product.name}', style: TextStyle(fontSize: 20)),
Text('价格: ¥${product.price}', style: TextStyle(fontSize: 20)),
],
),
),
);
}
}
练习2:实现城市选择器 ⭐⭐⭐
要求:
- 主页显示当前选择的城市
- 点击按钮打开城市列表
- 选择城市后返回主页
- 主页更新显示选择的城市
class CitySelectDemo extends StatefulWidget {
@override
State<CitySelectDemo> createState() => _CitySelectDemoState();
}
class _CitySelectDemoState extends State<CitySelectDemo> {
String _city = '北京';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('城市选择')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前城市:$_city', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => CityListPage(),
),
);
if (result != null) {
setState(() {
_city = result;
});
}
},
child: Text('选择城市'),
),
],
),
),
);
}
}
class CityListPage extends StatelessWidget {
final List<String> cities = ['北京', '上海', '广州', '深圳', '杭州'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('选择城市')),
body: ListView.builder(
itemCount: cities.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(cities[index]),
onTap: () {
Navigator.pop(context, cities[index]);
},
);
},
),
);
}
}
参考: 《Flutter实战·第二版》2.4节