Appearance
第8章:Flutter 路由与导航
8.1 路由的概念
什么是路由?
在 Flutter 中,路由是指页面之间的导航机制,类似于网页中的 URL 跳转。
路由的作用:
- 实现页面之间的跳转
- 传递数据和参数
- 管理页面栈
- 控制页面的进出场动画
路由的基本概念
- 路由栈:Flutter 使用栈(Stack)来管理路由,新页面入栈,返回时出栈
- 路由对象:每个页面都是一个
Widget,通过Navigator进行管理 - 路由名称:为路由分配一个唯一的字符串标识符,方便管理
8.2 基础路由(Navigator)
Navigator 简介
Navigator 是 Flutter 中用于管理路由的核心类,提供了基本的路由操作方法。
基本路由操作
跳转到新页面
使用 Navigator.push() 方法跳转到新页面:
dart
// 主页面
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 跳转到第二页
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondPage(),
),
);
},
child: const Text('Go to Second Page'),
),
),
);
}
}
// 第二页
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页
Navigator.pop(context);
},
child: const Text('Go Back'),
),
),
);
}
}返回上一页
使用 Navigator.pop() 方法返回上一页:
dart
// 返回上一页
Navigator.pop(context);
// 返回上一页并传递数据
Navigator.pop(context, '返回的数据');接收返回数据
使用 async/await 接收返回的数据:
dart
// 主页面
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () async {
// 跳转到第二页并等待返回数据
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondPage(),
),
);
// 显示返回的数据
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Received: $result')),
);
}
},
child: const Text('Go to Second Page'),
),
),
);
}
}
// 第二页
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页并传递数据
Navigator.pop(context, 'Hello from Second Page');
},
child: const Text('Go Back with Data'),
),
),
);
}
}8.3 命名路由
命名路由的优势
- 集中管理:所有路由在一个地方配置
- 代码简洁:使用路由名称跳转,无需创建
MaterialPageRoute - 参数传递:支持通过
arguments传递参数 - 路由守卫:可以通过
onGenerateRoute实现路由拦截
配置命名路由
在 MaterialApp 的 routes 属性中配置命名路由:
dart
void main() {
runApp(
MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/', // 默认路由
routes: {
'/': (context) => const HomePage(),
'/second': (context) => const SecondPage(),
'/third': (context) => const ThirdPage(),
},
),
);
}使用命名路由跳转
使用 Navigator.pushNamed() 方法通过路由名称跳转:
dart
// 跳转到第二页
Navigator.pushNamed(context, '/second');
// 跳转到第二页并传递参数
Navigator.pushNamed(
context,
'/second',
arguments: 'Hello from Home Page',
);接收路由参数
使用 ModalRoute.of(context)?.settings.arguments 接收参数:
dart
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
// 接收参数
final arguments = ModalRoute.of(context)?.settings.arguments;
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Received: $arguments'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Go Back'),
),
],
),
),
);
}
}命名路由返回数据
dart
// 跳转到第二页并等待返回数据
final result = await Navigator.pushNamed(context, '/second');
// 第二页返回数据
Navigator.pop(context, 'Hello from Second Page');8.4 路由守卫
什么是路由守卫?
路由守卫是一种拦截路由跳转的机制,可以在路由跳转前进行检查,如权限验证、登录状态检查等。
使用 onGenerateRoute 实现路由守卫
dart
void main() {
runApp(
MaterialApp(
title: 'Route Guard Demo',
initialRoute: '/',
onGenerateRoute: (settings) {
// 路由守卫逻辑
if (settings.name == '/profile') {
// 检查登录状态
if (!isLoggedIn) {
// 未登录,跳转到登录页
return MaterialPageRoute(
builder: (context) => const LoginPage(),
);
}
}
// 正常路由处理
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) => const HomePage());
case '/login':
return MaterialPageRoute(builder: (context) => const LoginPage());
case '/profile':
return MaterialPageRoute(builder: (context) => const ProfilePage());
default:
return MaterialPageRoute(builder: (context) => const NotFoundPage());
}
},
),
);
}完整示例
dart
import 'package:flutter/material.dart';
// 模拟登录状态
bool isLoggedIn = false;
void main() {
runApp(
MaterialApp(
title: 'Route Guard Demo',
initialRoute: '/',
onGenerateRoute: (settings) {
// 路由守卫逻辑
if (settings.name == '/profile') {
if (!isLoggedIn) {
return MaterialPageRoute(
builder: (context) => const LoginPage(),
);
}
}
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) => const HomePage());
case '/login':
return MaterialPageRoute(builder: (context) => const LoginPage());
case '/profile':
return MaterialPageRoute(builder: (context) => const ProfilePage());
default:
return MaterialPageRoute(builder: (context) => const NotFoundPage());
}
},
),
);
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/profile');
},
child: const Text('Go to Profile'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/login');
},
child: Text(isLoggedIn ? 'Logout' : 'Login'),
),
],
),
),
);
}
}
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 模拟登录
isLoggedIn = true;
Navigator.pop(context);
},
child: const Text('Login'),
),
),
);
}
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 模拟登出
isLoggedIn = false;
Navigator.pop(context);
},
child: const Text('Logout'),
),
),
);
}
}
class NotFoundPage extends StatelessWidget {
const NotFoundPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('404')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Page Not Found'),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/');
},
child: const Text('Go Home'),
),
],
),
),
);
}
}8.5 页面跳转动画
自定义页面跳转动画
使用 PageRouteBuilder 自定义页面跳转动画:
dart
// 跳转到新页面,使用自定义动画
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 淡入淡出动画
return FadeTransition(
opacity: animation,
child: child,
);
// 缩放动画
/*
return ScaleTransition(
scale: animation,
child: child,
);
*/
// 滑动动画
/*
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
*/
},
transitionDuration: const Duration(milliseconds: 500),
),
);常见动画类型
淡入淡出动画
dart
FadeTransition(
opacity: animation,
child: child,
)缩放动画
dart
ScaleTransition(
scale: animation,
child: child,
)滑动动画
dart
SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0), // 从右侧进入
end: Offset.zero,
).animate(animation),
child: child,
)旋转动画
dart
RotationTransition(
turns: animation,
child: child,
)8.6 实操案例:实现多页面跳转
目标
创建一个包含多个页面的应用,实现页面之间的跳转、参数传递和返回。
步骤 1:创建页面
HomePage.dart
dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/details',
arguments: 'Item 123',
);
},
child: const Text('Go to Details'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final result = await Navigator.pushNamed(context, '/settings');
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Settings saved: $result')),
);
}
},
child: const Text('Go to Settings'),
),
],
),
),
);
}
}DetailsPage.dart
dart
import 'package:flutter/material.dart';
class DetailsPage extends StatelessWidget {
const DetailsPage({super.key});
@override
Widget build(BuildContext context) {
final arguments = ModalRoute.of(context)?.settings.arguments;
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Selected: $arguments'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Go Back'),
),
],
),
),
);
}
}SettingsPage.dart
dart
import 'package:flutter/material.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _notifications = true;
bool _darkMode = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SwitchListTile(
title: const Text('Notifications'),
value: _notifications,
onChanged: (value) {
setState(() {
_notifications = value;
});
},
),
SwitchListTile(
title: const Text('Dark Mode'),
value: _darkMode,
onChanged: (value) {
setState(() {
_darkMode = value;
});
},
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {
Navigator.pop(
context,
{'notifications': _notifications, 'darkMode': _darkMode}
);
},
child: const Text('Save Settings'),
),
],
),
),
);
}
}步骤 2:配置路由
dart
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
import 'pages/details_page.dart';
import 'pages/settings_page.dart';
void main() {
runApp(
MaterialApp(
title: 'Navigation Demo',
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/details': (context) => const DetailsPage(),
'/settings': (context) => const SettingsPage(),
},
),
);
}步骤 3:运行应用
- 启动模拟器或连接真机
- 运行项目
- 测试页面跳转、参数传递和返回功能
8.7 路由管理最佳实践
1. 使用命名路由
- 集中管理所有路由
- 使用常量定义路由名称
- 方便维护和调试
2. 路由参数传递
- 使用
arguments参数传递数据 - 对于复杂参数,使用对象或 Map
- 接收参数时进行类型检查
3. 路由守卫
- 使用
onGenerateRoute实现权限控制 - 处理未找到的路由
- 统一路由处理逻辑
4. 动画效果
- 根据应用风格选择合适的动画
- 保持动画一致
- 避免过度动画影响用户体验
5. 路由栈管理
- 合理使用
push、pop、pushReplacement等方法 - 避免路由栈过深
- 处理返回逻辑
8.8 新手易错点
路由配置错误
错误:
- 忘记在
MaterialApp中配置路由 - 路由名称拼写错误
- 路由名称缺少斜杠
解决方案:
- 仔细检查路由配置
- 使用常量定义路由名称
- 确保路由名称以斜杠开头
参数传递错误
错误:
- 传递参数类型与接收类型不匹配
- 未处理 null 参数
- 参数传递方式错误
解决方案:
- 确保参数类型一致
- 使用
??或?.操作符处理 null - 正确使用
arguments参数
路由守卫逻辑错误
错误:
- 路由守卫逻辑过于复杂
- 未处理所有路由情况
- 循环跳转
解决方案:
- 保持路由守卫逻辑简洁
- 处理所有可能的路由情况
- 避免循环跳转
动画性能问题
错误:
- 动画持续时间过长
- 动画效果过于复杂
- 频繁触发动画
解决方案:
- 保持动画简洁
- 控制动画持续时间
- 避免频繁触发动画
8.9 小结
本章介绍了 Flutter 的路由与导航,包括基础路由、命名路由、路由守卫、页面跳转动画等内容。路由与导航是 Flutter 应用中不可或缺的一部分,掌握好路由管理对于构建流畅的用户体验至关重要。
我们学习了:
- 基础路由:使用
Navigator.push()和Navigator.pop()进行页面跳转 - 命名路由:集中管理路由,使用路由名称跳转
- 路由守卫:实现权限控制和路由拦截
- 页面跳转动画:自定义页面跳转效果
- 路由参数传递:在页面之间传递数据
通过实操案例,我们实现了一个包含多个页面的应用,体验了页面跳转、参数传递和返回的完整流程。在实际开发中,你应该根据应用的复杂度选择合适的路由管理方式。
在接下来的章节中,我们将学习 Flutter 的网络请求,掌握如何与后端接口交互,获取和处理数据。
