在移动应用开发中,路由(Route)和导航(Navigation)是实现页面跳转和用户导航体验的核心机制。Flutter 提供了一套完整的路由管理系统,能够轻松实现页面之间的切换、参数传递和返回数据等功能。本节课将详细介绍 Flutter 中的路由与导航机制,帮助你掌握页面跳转的各种实现方式。
一、页面跳转基本原理
Flutter 中的路由管理基于栈(Stack)数据结构,使用 Navigator
组件管理路由栈,实现页面的入栈(push)和出栈(pop)操作。
1. Navigator 与路由栈
Navigator
是 Flutter 中管理路由的核心组件,它维护一个路由栈,每个路由对应一个页面:
- 新页面打开时,对应的路由被 "推入"(push)栈顶
- 页面关闭时,对应的路由从栈顶 "弹出"(pop)
- 当前显示的页面始终是栈顶的路由
在 Flutter 应用中,MaterialApp
会自动提供一个 Navigator
,我们可以通过 Navigator.of(context)
获取其实例进行操作。
2. MaterialPageRoute
MaterialPageRoute
是 Material Design 风格的路由,它会自动处理页面切换时的过渡动画(如左右滑动、淡入淡出等),适用于大多数场景。
基本用法示例:
dart
// 第一个页面
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 导航到第二个页面
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
},
child: const Text('Go to Second Screen'),
),
),
);
}
}
// 第二个页面
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
// 导航栏返回按钮
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
// 返回上一个页面
Navigator.pop(context);
},
),
),
body: const Center(child: Text('This is the second screen')),
);
}
}
注意 :在 Scaffold
中,AppBar 会自动添加返回按钮,点击时会触发 Navigator.pop(context)
,因此通常不需要手动实现返回按钮。
3. 页面切换动画
MaterialPageRoute
提供了一些属性来定制页面切换动画:
dart
MaterialPageRoute(
builder: (context) => const DetailScreen(),
// 过渡动画持续时间
transitionDuration: const Duration(milliseconds: 500),
// 反向过渡动画持续时间
reverseTransitionDuration: const Duration(milliseconds: 300),
// 是否维护页面状态
maintainState: true,
// 是否为全屏对话框
fullscreenDialog: true, // 会使用上下滑动动画
)
当 fullscreenDialog
设为 true
时,页面会从底部滑入,类似对话框的动画效果。
二、命名路由配置
对于复杂应用,直接使用 MaterialPageRoute
可能导致代码冗余且不易维护。Flutter 提供了命名路由(Named Routes)机制,通过路由名称进行页面跳转,使路由管理更加集中和规范。
1. 基本配置
在 MaterialApp
中通过 routes
属性配置命名路由:
dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/', // 初始路由(首页)
routes: {
// 路由名称到页面的映射
'/': (context) => const HomeScreen(),
'/second': (context) => const SecondScreen(),
'/detail': (context) => const DetailScreen(),
},
);
}
}
2. 使用命名路由跳转
通过 Navigator.pushNamed()
方法根据路由名称进行跳转
dart
// 从首页跳转到第二个页面
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: const Text('Go to Second Screen'),
)
// 从第二个页面返回首页
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// 或者跳转到指定路由(会替换当前路由栈)
// Navigator.pushReplacementNamed(context, '/');
},
child: const Text('Go Back'),
)
常用的命名路由导航方法:
Navigator.pushNamed(context, '/routeName')
:跳转到指定路由Navigator.popAndPushNamed(context, '/routeName')
:弹出当前路由并跳转到新路由Navigator.pushReplacementNamed(context, '/routeName')
:替换当前路由Navigator.pushNamedAndRemoveUntil(context, '/routeName', (route) => false)
:跳转到新路由并移除之前的所有路由
三、路由参数传递与接收
在页面跳转时,经常需要传递参数(如 ID、名称等)。Flutter 提供了多种方式在路由之间传递和接收参数。
1. 基本参数传递
使用 Navigator.pushNamed()
的 arguments
参数传递数据:
dart
// 传递参数
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/detail',
arguments: {
'id': 123,
'title': 'Flutter 路由教程',
},
);
},
child: const Text('查看详情'),
)
在目标页面中,使用 ModalRoute.of(context)!.settings.arguments
接收参数:
dart
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
// 接收参数
final Map<String, dynamic> args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return Scaffold(
appBar: AppBar(title: Text(args['title'])),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ID: ${args['id']}'),
const SizedBox(height: 16),
Text('Title: ${args['title']}'),
],
),
),
);
}
}
2. 类型安全的参数传递
为了避免类型转换错误,可以创建专门的参数类,实现类型安全的参数传递:
dart
import 'package:flutter/material.dart';
// 参数类
class DetailArguments {
final int id;
final String title;
DetailArguments({required this.id, required this.title});
}
// 传递参数
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/detail',
arguments: DetailArguments(
id: 123,
title: 'Flutter 路由教程',
),
);
},
child: const Text('查看详情'),
)
// 接收参数
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
// 类型安全的参数接收
final DetailArguments args =
ModalRoute.of(context)!.settings.arguments as DetailArguments;
return Scaffold(
appBar: AppBar(title: Text(args.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ID: ${args.id}'),
const SizedBox(height: 16),
Text('Title: ${args.title}'),
],
),
),
);
}
}
四、返回数据与 Navigator.pop (context, result)
不仅可以向前跳转时传递参数,还可以在返回上一个页面时携带数据,这在处理表单提交、选择器等场景非常有用。
1. 返回数据的基本用法
在第二个页面中,通过 Navigator.pop(context, result)
返回数据:
dart
import 'package:flutter/material.dart';
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Select an Option')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// 返回数据
Navigator.pop(context, 'Option 1');
},
child: const Text('Option 1'),
),
ElevatedButton(
onPressed: () {
// 返回数据
Navigator.pop(context, 'Option 2');
},
child: const Text('Option 2'),
),
],
),
),
);
}
}
在第一个页面中,通过 async/await
获取返回的数据:
dart
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({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 SelectionScreen()),
);
// 处理返回结果
if (result != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Selected: $result')));
}
},
child: const Text('Go to Selection Screen'),
),
),
);
}
}
2. 使用命名路由返回数据
使用命名路由时,同样可以返回数据:
dart
// 跳转并等待结果
final result = await Navigator.pushNamed(context, '/selection');
// 返回数据
Navigator.pop(context, 'Selected Value');
五、路由钩子
Flutter 提供了路由钩子(Route Hooks)机制,允许在路由跳转过程中进行拦截和处理,实现更灵活的路由管理。
1. onGenerateRoute
onGenerateRoute
用于动态生成路由,当 routes
中没有匹配的路由名称时,会调用该方法。它适用于需要动态生成路由或对路由进行统一处理的场景(如参数验证、权限检查等)。
dart
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/second': (context) => const SecondScreen(),
},
// 路由生成器
onGenerateRoute: (settings) {
// 根据路由名称处理
if (settings.name == '/detail') {
// 验证参数
final args = settings.arguments;
if (args is DetailArguments) {
return MaterialPageRoute(
builder: (context) => DetailScreen(args: args),
);
} else {
// 参数无效时导航到错误页面
return MaterialPageRoute(
builder: (context) => const ErrorScreen(message: 'Invalid arguments'),
);
}
}
// 其他未匹配的路由
return null;
},
)
使用 onGenerateRoute
重构详情页:
dart
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
final DetailArguments args;
const DetailScreen({
super.key,
required this.args,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(args.title)),
body: Center(
child: Text('ID: ${args.id}'),
),
);
}
}
2. onUnknownRoute
当 routes
和 onGenerateRoute
都无法匹配路由名称时,会调用 onUnknownRoute
。通常用于处理 404 情况,显示一个 "页面未找到" 的提示页面。
dart
MaterialApp(
// ...其他配置
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => NotFoundScreen(
routeName: settings.name,
),
);
},
)
// 未找到页面
import 'package:flutter/material.dart';
class NotFoundScreen extends StatelessWidget {
final String? routeName;
const NotFoundScreen({
super.key,
this.routeName,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page Not Found')),
body: Center(
child: Text('The page "$routeName" does not exist.'),
),
);
}
}
3. 路由拦截与权限控制
利用 onGenerateRoute
可以实现路由拦截和权限控制,例如检查用户是否登录:
dart
onGenerateRoute: (settings) {
// 需要登录的路由列表
final requiredAuthRoutes = ['/profile', '/settings'];
// 检查是否需要登录且用户未登录
if (requiredAuthRoutes.contains(settings.name) && !isLoggedIn()) {
// 导航到登录页面,并记录目标路由
return MaterialPageRoute(
builder: (context) => LoginScreen(
redirectRoute: settings.name,
),
);
}
// 正常处理其他路由
// ...
}
六、路由管理进阶
对于大型应用,推荐使用路由管理库(如 auto_route
、fluro
等)来简化路由管理。但即使使用这些库,理解 Flutter 原生路由机制仍然很重要。
1. 路由别名与模块化
在大型应用中,可以将路由按模块进行划分,使用路由别名统一管理:
dart
// 路由常量类
class AppRoutes {
static const String home = '/';
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String profile = '/user/profile';
static const String settings = '/user/settings';
static const String productList = '/products';
static const String productDetail = '/products/detail';
}
// 使用时
Navigator.pushNamed(context, AppRoutes.productDetail);
2. 路由动画定制
除了默认的过渡动画,还可以自定义路由切换动画:
dart
import 'package:flutter/material.dart';
class CustomPageRoute<T> extends PageRoute<T> {
final Widget child;
CustomPageRoute({required this.child});
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
bool get maintainState => true;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
// 自定义动画
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
}
}
// 使用自定义路由
Navigator.push(
context,
CustomPageRoute(child: const DetailScreen()),
);
七、实例:完整的路由管理应用
下面实现一个包含多个页面、参数传递和返回数据功能的完整应用:
dart
// 1. 路由常量
import 'package:flutter/material.dart';
class Routes {
static const String home = '/';
static const String userList = '/users';
static const String userDetail = '/users/detail';
static const String settings = '/settings';
}
// 2. 用户模型
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
}
// 3. 主应用
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Route Demo',
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: Routes.home,
onGenerateRoute: (settings) {
switch (settings.name) {
case Routes.home:
return MaterialPageRoute(builder: (context) => const HomePage());
case Routes.userList:
return MaterialPageRoute(builder: (context) => UserListPage());
case Routes.userDetail:
final user = settings.arguments as User;
return MaterialPageRoute(
builder: (context) => UserDetailPage(user: user),
);
case Routes.settings:
return MaterialPageRoute(
builder: (context) => const SettingsPage(),
);
default:
return MaterialPageRoute(
builder: (context) => NotFoundPage(route: settings.name),
);
}
},
);
}
}
// 4. 首页
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, Routes.userList),
child: const Text('View User List'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
final result = await Navigator.pushNamed(context, Routes.settings);
if (result == true) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings saved')),
);
}
},
child: const Text('Go to Settings'),
),
],
),
);
}
}
// 5. 用户列表页
class UserListPage extends StatelessWidget {
UserListPage({super.key});
// 模拟用户数据
final List<User> users = [
User(id: 1, name: 'John Doe', email: 'john@example.com'),
User(id: 2, name: 'Jane Smith', email: 'jane@example.com'),
User(id: 3, name: 'Bob Johnson', email: 'bob@example.com'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User List')),
body: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
onTap: () {
Navigator.pushNamed(
context,
Routes.userDetail,
arguments: user,
);
},
);
},
),
);
}
}
// 6. 用户详情页
class UserDetailPage extends StatelessWidget {
final User user;
const UserDetailPage({
super.key,
required this.user,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(user.name)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${user.id}'),
const SizedBox(height: 8),
Text('Name: ${user.name}'),
const SizedBox(height: 8),
Text('Email: ${user.email}'),
],
),
),
);
}
}
// 7. 设置页面
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _notificationsEnabled = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
children: [
SwitchListTile(
title: const Text('Enable Notifications'),
value: _notificationsEnabled,
onChanged: (value) {
setState(() {
_notificationsEnabled = value;
});
},
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
// 返回保存成功的结果
Navigator.pop(context, true);
},
child: const Text('Save Settings'),
),
),
],
),
);
}
}
// 8. 未找到页面
class NotFoundPage extends StatelessWidget {
final String? route;
const NotFoundPage({
super.key,
this.route,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page Not Found')),
body: Center(
child: Text('The page "$route" does not exist.'),
),
);
}
}