路由基础概念
在 Flutter 中,路由(Route) 代表应用中的一个页面。页面之间的跳转由 Navigator 管理,它维护一个 路由栈(Stack):
┌─────────────┐
│ 详情页 │ ← 栈顶(当前可见页面)
├─────────────┤
│ 列表页 │
├─────────────┤
│ 首页 │ ← 栈底
└─────────────┘
| 操作 | API | 效果 |
|---|---|---|
| 打开新页面 | Navigator.push |
压入栈顶 |
| 返回上一页 | Navigator.pop |
弹出栈顶 |
| 命名跳转 | Navigator.pushNamed |
通过路由名跳转 |
路由配置通常写在 MaterialApp 上,它是整个应用的入口容器。
第一阶段:基本路由
1.1 设置首页
最简单的方式是通过 home 指定应用启动后显示的第一个页面:
dart
return MaterialApp(home: ListPage());
1.2 跳转到新页面
列表项点击时,使用 Navigator.push 配合 MaterialPageRoute 打开详情页:
dart
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(message: "这是第$index个详情"),
),
);
三个关键要素:
| 要素 | 说明 |
|---|---|
context |
当前 Widget 的上下文,Navigator 通过它定位路由栈 |
MaterialPageRoute |
Material 风格的页面路由,负责页面切换动画 |
builder |
返回目标页面的 Widget |
1.3 接收参数
详情页通过 构造函数 接收参数,这是基本路由传参最直接的方式:
dart
class DetailPage extends StatelessWidget {
final String message;
DetailPage({Key? key, required this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text("详情页-${this.message}")),
);
}
}
1.4 阶段小结
- 适合页面少、跳转关系简单的场景
- 每次跳转都要写
MaterialPageRoute,路由逻辑分散在各处 - 传参依赖构造函数,类型安全、IDE 提示友好
第二阶段:基本路由传参与返回
在第一阶段基础上,本节重点学习 传参 和 返回。
2.1 传递不同类型的参数
列表页跳转时传入 id(整型):
dart
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(id: index + 1),
),
);
详情页用 StatefulWidget 接收,在 initState 中读取:
dart
class DetailPage extends StatefulWidget {
final int? id;
DetailPage({Key? key, required this.id}) : super(key: key);
}
class _DetailPageState extends State<DetailPage> {
@override
void initState() {
super.initState();
print(widget.id); // 通过 widget.id 访问参数
}
}
2.2 返回上一页
使用 Navigator.pop(context) 关闭当前页面,回到栈中上一个页面:
dart
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("返回列表页${widget.id}"),
)
2.3 带返回值 pop(扩展知识)
Navigator.pop 还可以向上一页传递数据:
dart
// 详情页返回时
Navigator.pop(context, {"result": "操作成功"});
// 列表页接收(需要用 async/await)
final result = await Navigator.push(...);
print(result); // {"result": "操作成功"}
2.4 阶段小结
| 能力 | API |
|---|---|
| 跳转 | Navigator.push + MaterialPageRoute |
| 传参 | 目标页构造函数 |
| 返回 | Navigator.pop(context) |
| 带值返回 | Navigator.pop(context, data) |
第三阶段:命名路由
当页面增多、跳转关系复杂时,应改用 命名路由 ------在 MaterialApp 中集中注册路由表。
3.1 注册路由表
dart
return MaterialApp(
initialRoute: "/detail",
routes: {
"/list": (context) => ListPage(),
"/detail": (context) => DetailPage(),
},
theme: ThemeData(primarySwatch: Colors.blue),
);
| 配置项 | 作用 |
|---|---|
routes |
路由名 → 页面 Widget 的映射表 |
initialRoute |
应用启动时打开的命名路由 |
3.2 命名跳转
不再手写 MaterialPageRoute,一行即可跳转:
dart
Navigator.pushNamed(context, "/list");
对比基本路由:
dart
// 基本路由 --- 需要知道目标 Widget
Navigator.push(context, MaterialPageRoute(builder: (context) => DetailPage()));
// 命名路由 --- 只需知道路由名
Navigator.pushNamed(context, "/detail");
3.3 基本路由与命名路由混用
列表页使用 Navigator.push + MaterialPageRoute,详情页用 pushNamed 返回列表。两种写法可以共存,但生产环境建议统一为一种风格。
3.4 注意事项
home与initialRoute不要同时使用。同时写了
initialRoute: "/detail"和home: ListPage(),后者会覆盖前者。实际使用时二选一即可。
3.5 阶段小结
- 路由集中管理,跳转代码更简洁
- 适合中大型应用
- 传参需要额外机制(见下一阶段)
第四阶段:命名路由传参
命名路由无法通过构造函数直接传参,需借助 arguments 参数。
4.1 发送参数
跳转时通过 arguments 传递 Map:
dart
Navigator.pushNamed(
context,
"/detail",
arguments: {"id": index + 1},
);
arguments 类型为 Object?,通常传 Map<String, dynamic>。
4.2 接收参数
目标页通过 ModalRoute.of(context) 获取当前路由信息:
dart
@override
void initState() {
super.initState();
Future.microtask(() {
if (ModalRoute.of(context) != null) {
Map<String, dynamic> arguments =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
print("从列表页传递过来的参数:${arguments["id"]}");
} else {
print("获取不到参数");
}
});
}
为什么用 Future.microtask?
在 initState 中直接调用 ModalRoute.of(context) 时,Widget 可能尚未挂载到路由树,context 还不可用。Future.microtask 将读取推迟到当前帧结束后,确保路由信息已就绪。
4.3 在 build 中接收(替代写法)
也可以在 build 方法中读取,无需 Future.microtask:
dart
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
final id = args?["id"];
return Scaffold(body: Text("详情 id: $id"));
}
4.4 两种传参方式对比
| 方式 | 适用场景 | 类型安全 | 写法 |
|---|---|---|---|
| 构造函数传参 | 基本路由 | 强类型 | DetailPage(id: 1) |
| arguments 传参 | 命名路由 | 需手动 cast | arguments: {"id": 1} |
4.5 阶段小结
- 命名路由传参依赖
arguments+ModalRoute.of(context) - 注意
initState中获取参数的时机问题 - 生产环境建议封装路由工具类,避免到处写 cast
第五阶段:路由管理与守卫
这是路由学习的 进阶阶段,涵盖动态路由生成、404 处理和登录拦截。
5.1 完整路由架构
dart
return MaterialApp(
initialRoute: "goodsList",
routes: {
"goodsList": (context) => GoodsListPage(),
},
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (context) => NotFoundPage());
},
onGenerateRoute: (settings) {
if (settings.name == "cartList") {
bool isLogin = false;
if (isLogin) {
return MaterialPageRoute(builder: (context) => CartListPage());
} else {
return MaterialPageRoute(builder: (context) => LoginPage());
}
} else if (settings.name == "login") {
return MaterialPageRoute(builder: (context) => LoginPage());
}
return null; // 建议显式返回 null,交给 onUnknownRoute 处理
},
);
5.2 路由匹配优先级
Flutter 按以下顺序匹配路由:
1. routes 表中查找
↓ 未找到
2. onGenerateRoute 动态生成
↓ 返回 null 或未处理
3. onUnknownRoute 兜底(404)
流程图:
pushNamed("cartList")
│
▼
routes 中有吗? ──是──→ 直接打开对应页面
│否
▼
onGenerateRoute ──→ 动态判断(登录守卫等)
│返回 null
▼
onUnknownRoute ──→ 404 页面
5.3 onGenerateRoute:动态路由与登录守卫
onGenerateRoute 在 routes 找不到匹配项时触发,适合:
- 需要 条件判断 的路由(如登录拦截)
- 路由表过大、不便全部写在
routes中的场景 - 需要根据
settings.arguments动态创建页面的场景
项目中的 登录守卫 示例:
dart
if (settings.name == "cartList") {
bool isLogin = false; // 实际项目中从全局状态或本地存储读取
if (isLogin) {
return MaterialPageRoute(builder: (context) => CartListPage());
} else {
return MaterialPageRoute(builder: (context) => LoginPage()); // 未登录跳转登录页
}
}
这是电商类 App 的常见模式:访问购物车前检查登录状态。
5.4 onUnknownRoute:404 兜底
当路由名既不在 routes 中,onGenerateRoute 也未处理时,触发 onUnknownRoute:
dart
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (context) => NotFoundPage());
},
项目中商品列表页点击按钮跳转到 "abc"(不存在的路由),最终会进入 404 页面:
dart
Navigator.pushNamed(context, "abc"); // → NotFoundPage
5.5 页面结构
| 页面 | 路由名 | 注册方式 |
|---|---|---|
| 商品列表 | goodsList |
routes 静态注册 |
| 购物车 | cartList |
onGenerateRoute + 登录守卫 |
| 登录页 | login |
onGenerateRoute |
| 未知页面 | --- | onUnknownRoute 兜底 |
5.6 阶段小结
| API | 用途 |
|---|---|
routes |
静态路由表,无条件的固定页面 |
onGenerateRoute |
动态路由、登录守卫、复杂传参 |
onUnknownRoute |
404 兜底,提升用户体验 |
initialRoute |
指定启动页(与 home 二选一) |
常用 Navigator API 速查
| API | 说明 | 示例 |
|---|---|---|
Navigator.push |
压入新页面 | Navigator.push(context, MaterialPageRoute(...)) |
Navigator.pop |
弹出当前页 | Navigator.pop(context) |
Navigator.pushNamed |
命名路由跳转 | Navigator.pushNamed(context, "/detail") |
Navigator.popUntil |
弹出直到满足条件 | Navigator.popUntil(context, ModalRoute.withName("/home")) |
Navigator.pushReplacement |
替换当前页(不可返回) | 登录成功后跳转首页 |
Navigator.pushNamedAndRemoveUntil |
跳转并清空栈 | 退出登录回登录页 |
生产环境进阶建议
1. 封装路由工具类
避免路由名硬编码散落各处:
dart
class AppRoutes {
static const goodsList = "goodsList";
static const cartList = "cartList";
static const login = "login";
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case cartList:
return _guardCart(settings);
case login:
return MaterialPageRoute(builder: (_) => LoginPage());
default:
return null;
}
}
}
2. 使用 go_router 等第三方库
Flutter 官方推荐的 go_router 支持:
- 声明式路由
- 深度链接(Deep Link)
- 路由重定向(Redirect)
- 更清晰的嵌套路由
当项目路由复杂度继续增长时,值得迁移。
3. 统一参数模型
为命名路由定义参数类,替代裸 Map:
dart
class DetailArgs {
final int id;
DetailArgs({required this.id});
}
// 跳转
Navigator.pushNamed(context, "/detail", arguments: DetailArgs(id: 1));
// 接收
final args = ModalRoute.of(context)!.settings.arguments as DetailArgs;
4. 修复 onGenerateRoute 的返回值
当前 路由管理.dart 中,未匹配的路由分支没有显式 return null,建议补上,让 onUnknownRoute 可靠兜底:
dart
onGenerateRoute: (settings) {
if (settings.name == "cartList") { ... }
else if (settings.name == "login") { ... }
return null; // 显式返回 null,触发 onUnknownRoute
},
总结
Flutter 路由学习可以归纳为一条清晰的路径:
基本路由(push + MaterialPageRoute)
↓ 加参数、加返回
基本路由传参(构造函数 + pop)
↓ 集中管理
命名路由(routes + pushNamed)
↓ 命名路由传参
arguments + ModalRoute
↓ 动态控制
路由管理(onGenerateRoute + onUnknownRoute + 守卫)