1. 前言
在前面的博客中,我们一直只使用单页面(一个 Scaffold)。但真正的 App 必然包含多个页面:列表页 → 详情页 → 设置页 → 返回并传递数据等。
Flutter 中的页面跳转和管理称为路由(Route) ,而管理路由的组件称为导航器(Navigator) 。本篇将从最基础的 Navigator.push 和 pop 讲起,然后介绍命名路由的集中管理,最后推荐现代化的声明式路由方案 ------ go_router。
2. 基础路由:push 和 pop
核心思想 :Flutter 将页面放入一个栈中, push 添加新页面, pop 移除栈顶页面返回上一页。
示例:从首页跳转到详情页
less
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade300,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
},
child: Text('详情页'),
),
),
详情页
less
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('详情页')),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade300,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () {
Navigator.pop(context);
},
child: Text('返回'),
),
),
);
}
}
传参到详情页:通过构造函数传递参数。
less
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
final String title;
final String content;
const DetailPage({super.key, required this.title, required this.content});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade300,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () {
Navigator.pop(context);
},
child: Text(content),
),
),
);
}
}
less
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade300,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailPage(title: '详情页', content: '这是详情页的内容'),
),
);
},
child: Text('详情页'),
),
),
从详情页返回数据 :使用 Navigator.pop 带回结果,并在首页使用 async/await 接收。 详情页部分代码
vbnet
onPressed: () {
Navigator.pop(context, '这是详情页返回的内容');
},
主页部分代码
ini
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailPage(title: '详情页', content: '这是详情页的内容'),
),
);
if (result != null) {
setState(() {
message = result;
});
}
},
3. 命名路由:集中管理页面路径
当 App 页面增多时,到处使用 MaterialPageRoute 会导致代码重复,且页面路径散落在各处。命名路由允许你在一个地方注册所有页面,然后通过字符串路径跳转。
步骤 1:在 MaterialApp 中定义 routes
部分代码,完整代码请看最后的gitee链接
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: '我的第一篇Flutter'),
routes: {
'/greeting': (context) => const Greetingwidget(name: 'Gitrocr'),
'/changeable': (context) => const Changeabletextwidget(),
'/user': (context) => const UserPage(),
'/simple_counter': (context) => const SimpleCounterProvider(),
'/todo': (context) => TodoPage(),
'/post_list': (context) => PostListPage(),
'/post_list_with_provider': (context) => PostListPageWithProvider(),
},
);
}
}
步骤 2:使用 Navigator.pushNamed 跳转
部分代码,完整代码请看最后的gitee链接
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade300,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () {
Navigator.pushNamed(context, '/greeting');
},
child: Text('欢迎学习 Flutter'),
),
步骤 3:传递参数(使用 arguments)
部分代码,完整代码请看最后的gitee链接
onPressed: () {
Navigator.pushNamed(
context,
'/greeting',
arguments: {'title': 'Gitrocr'},
);
},
部分代码,完整代码请看最后的gitee链接
import 'package:flutter/material.dart';
class Greetingwidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>;
final title = args['title'];
return Scaffold(
appBar: AppBar(title: Text(title)),
);
}
}
命名路由的缺点:参数类型不安全(需要手动转换),且无法轻易实现深层嵌套路由(比如底部导航栏内的页面跳转)。
4. 现代化方案:go_router(推荐)
go_router 是 Flutter 官方推荐的声明式路由包,它解决了命名路由的痛点,支持路径参数、嵌套路由、重定向、过渡动画等。
添加依赖:
yaml
dependencies:
go_router: ^14.0.0 # 请使用最新版本
基本用法:
php
import 'package:go_router/go_router.dart';
// 1. 定义路由配置
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/detail/:id', // 路径参数
name: 'detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
// 也可以传递额外参数
final extra = state.extra as String?;
return DetailPage(id: id, extra: extra);
},
),
],
);
// 2. 在 MaterialApp 中使用
MaterialApp.router(
routerConfig: _router,
// 不再需要 home 和 routes
)
// 3. 跳转
context.go('/detail/123'); // 替换当前路由
context.push('/detail/456'); // 压入新页面
context.pushNamed('detail', pathParameters: {'id': '789'}, extra: '自定义数据');
// 4. 返回
context.pop();
嵌套路由(例如底部导航栏 + 多个子页面):
less
final GoRouter _router = GoRouter(
initialLocation: '/home',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => HomePage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => ProfilePage(),
),
],
),
],
),
],
);
然后在 ScaffoldWithNavBar 中使用 navigationShell.currentIndex 和 onTap 来切换底部导航。
优点总结:
- 类型安全的参数传递(路径参数和额外对象)
- 支持深链、重定向、守卫(权限控制)
- 与 Flutter Web 完美兼容(路径与 URL 同步)
- 嵌套路由非常清晰
5. 实战:将之前的帖子列表改为支持详情页
结合我们第五篇的网络请求,为每个帖子添加点击跳转详情页。
第一步:定义详情页
less
class PostDetailPage extends StatelessWidget {
final Post post;
const PostDetailPage({super.key, required this.post});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(post.title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${post.id}', style: TextStyle(color: Colors.grey)),
SizedBox(height: 16),
Text(post.body, style: TextStyle(fontSize: 16)),
],
),
),
);
}
}
第二步:配置 go_router(假设我们已有 Post 模型)
php
GoRoute(
path: '/post/:id',
name: 'post',
builder: (context, state) {
final id = int.parse(state.pathParameters['id']!);
// 从 Provider 或直接传入 Post 对象
final post = state.extra as Post?;
if (post != null) {
return PostDetailPage(post: post);
}
// 如果没有传 extra,可能需要从缓存中根据 id 获取,简化起见先返回一个空页面
return PostDetailPage(post: Post(id: id, title: '加载中...', body: ''));
},
)
第三步:在列表项中跳转
css
ListTile(
title: Text(post.title),
subtitle: Text(post.body),
onTap: () {
context.pushNamed('post', pathParameters: {'id': post.id.toString()}, extra: post);
},
)
第四步:支持返回刷新 (如果需要列表页在返回时刷新数据,可以在详情页 pop 之前调用某些回调,或使用 context.push 后等待 context.pop 的返回值)。
6. 路由传参对比总结
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
构造函数 + MaterialPageRoute |
简单项目,页面少 | 直观,类型安全 | 耦合度高,难以统一管理 |
| 命名路由 + arguments | 中型项目 | 集中管理,解耦 | 参数类型不安全,需要手动解析 |
| go_router | 中大型项目,需要嵌套、深链 | 声明式,类型安全,功能全面 | 学习成本稍高,需要额外依赖 |
7. 常见问题与避坑
| 问题 | 原因 | 解决 |
|---|---|---|
Navigator.pop 后数据没传回来 |
没有在 pop 中传递参数,或没有用 await 接收 | 确保 pop 时带参数,且用 async 等待 |
使用 go_router 时 context.pop 不工作 |
context 不是路由上下文 |
确保调用 pop 的组件在 MaterialApp.router 之下,且使用了正确的 BuildContext |
| 页面跳转动画不想要 | 默认有左右滑动动画 | 在 go_router 中可设置 transitionsBuilder 自定义,或用 PageRouteBuilder |
| 深层嵌套路由返回时跳过了中间页面 | 使用了 go 而不是 push |
go 会替换整个栈,push 才会压栈;返回请用 pop |
8. 小结与作业
本篇我们学习了:
- 基础路由:
Navigator.push/pop,传参与返回数据 - 命名路由:集中管理页面路径,使用 arguments 传参
- go_router:现代化的声明式路由,支持路径参数、嵌套路由、类型安全
作业(巩固练习) :
- 将第五篇的帖子列表改造成使用 go_router,并实现点击进入详情页。
- 添加一个设置页面,通过命名路由(或 go_router)跳转,并在设置页面中修改主题颜色或字体大小,返回后主页面样式改变(提示:可使用 Provider 共享主题状态)。
- 实现一个简单的登录流程:未登录时自动重定向到登录页,登录成功后才能访问首页。(go_router 的
redirect功能)
掌握了路由与导航,你已经能够组织起多页面的复杂 App 了。下一篇博客预告:本地存储 ------ 使用 SharedPreferences 和 sqflite 保存数据,实现用户偏好设置和离线缓存。