📚 目录
核心概念
什么是导航和路由?
- 路由(Route):表示应用中的一个页面或屏幕。在 Flutter 中,每个页面都是一个 Widget。
- 导航器(Navigator):管理路由堆栈的组件,负责在路由之间进行跳转和管理。
- 路由堆栈(Route Stack):类似浏览器的历史记录,使用后进先出(LIFO)的方式管理页面。
导航的基本原理
路由堆栈(从下到上):
┌─────────────┐
│ 页面 C │ ← 当前页面(栈顶)
├─────────────┤
│ 页面 B │
├─────────────┤
│ 页面 A │ ← 初始页面(栈底)
└─────────────┘
- push:将新页面推入堆栈顶部
- pop:从堆栈顶部移除当前页面
- replace:替换当前页面
- popUntil:返回到指定页面
基本导航
1. Navigator.push() - 导航到新页面
使用 Navigator.push() 导航到新页面,这是最常用的导航方式。
// 基本用法
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
// 或者使用 Navigator.of(context)
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
完整示例:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '导航示例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const FirstScreen(),
);
}
}
// 第一个页面
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第一页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 导航到第二个页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
},
child: const Text('前往第二页'),
),
),
);
}
}
// 第二个页面
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第二页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页
Navigator.pop(context);
},
child: const Text('返回'),
),
),
);
}
}
2. Navigator.pop() - 返回上一页
使用 Navigator.pop() 返回上一页。
Navigator.pop(context);
// 可以返回数据
Navigator.pop(context, '返回的数据');
3. MaterialPageRoute vs CupertinoPageRoute
- MaterialPageRoute:Android 风格的页面过渡动画
- CupertinoPageRoute:iOS 风格的页面过渡动画
// Material 风格(Android)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
// Cupertino 风格(iOS)
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => SecondScreen()),
);
4. 自定义页面过渡动画
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 淡入淡出
return FadeTransition(opacity: 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: 300),
),
);
命名路由
命名路由可以减少代码重复,特别适合在多个地方导航到同一页面。
1. 定义路由表
在 MaterialApp 中定义路由表:
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
2. 使用命名路由导航
// 导航到命名路由
Navigator.pushNamed(context, '/details');
// 或者使用 pushReplacementNamed 替换当前路由
Navigator.pushReplacementNamed(context, '/details');
3. 完整示例
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '命名路由示例',
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/details');
},
child: const Text('前往详情页'),
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
child: const Text('前往设置页'),
),
],
),
),
);
}
}
⚠️ 注意事项
虽然命名路由提供了便利,但在处理深层链接和复杂导航需求时存在一定限制。对于大多数现代应用,推荐使用 Router API(见下文)。
路由传参
1. 通过构造函数传参
这是最简单直接的方式:
// 定义接收参数的页面
class DetailsScreen extends StatelessWidget {
final String title;
final int id;
const DetailsScreen({
super.key,
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) => DetailsScreen(
title: '详情',
id: 123,
),
),
);
2. 通过命名路由传参
使用 arguments 参数:
// 定义路由时使用 settings.arguments
MaterialApp(
routes: {
'/details': (context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return DetailsScreen(
title: args['title'],
id: args['id'],
);
},
},
);
// 导航时传递参数
Navigator.pushNamed(
context,
'/details',
arguments: {
'title': '详情',
'id': 123,
},
);
3. 使用路由生成器传参(推荐)
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/details') {
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (context) => DetailsScreen(
title: args['title'],
id: args['id'],
),
);
}
return null;
},
);
返回数据
从新页面返回数据到上一页:
1. 返回数据
// 在第二个页面返回数据
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择选项')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context, '选项A');
},
child: const Text('选择选项A'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context, '选项B');
},
child: const Text('选择选项B'),
),
],
),
),
);
}
}
2. 接收返回的数据
// 在第一个页面接收数据
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('选择了: $result')),
);
}
},
child: const Text('打开选择页面'),
);
3. 完整示例
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});
@override
State<FirstScreen> createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
String? _selectedOption;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第一页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_selectedOption != null)
Text('已选择: $_selectedOption'),
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
if (result != null) {
setState(() {
_selectedOption = result as String;
});
}
},
child: const Text('选择选项'),
),
],
),
),
);
}
}
路由生成器
onGenerateRoute 允许动态生成路由,适合处理复杂的路由逻辑。
1. 基本用法
MaterialApp(
onGenerateRoute: (settings) {
// 根据路由名称生成不同的页面
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const HomeScreen());
case '/details':
return MaterialPageRoute(builder: (_) => const DetailsScreen());
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsScreen());
default:
return MaterialPageRoute(
builder: (_) => const NotFoundScreen(),
);
}
},
);
2. 处理未知路由
MaterialApp(
onGenerateRoute: (settings) {
// 处理已知路由
if (settings.name == '/details') {
return MaterialPageRoute(builder: (_) => const DetailsScreen());
}
// 处理未知路由
return MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(child: Text('页面未找到')),
),
);
},
// 或者使用 onUnknownRoute
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (_) => const NotFoundScreen(),
);
},
);
3. 结合参数使用
MaterialApp(
onGenerateRoute: (settings) {
final uri = Uri.parse(settings.name ?? '/');
switch (uri.path) {
case '/details':
final id = uri.queryParameters['id'];
return MaterialPageRoute(
builder: (_) => DetailsScreen(id: id),
);
default:
return MaterialPageRoute(builder: (_) => const HomeScreen());
}
},
);
高级路由(Router API)
对于具有复杂导航需求的应用,Flutter 提供了 Router API,特别适用于处理深层链接和 Web URL 同步。
1. 使用 go_router(推荐)
go_router 是一个流行的路由包,简化了 Router 的使用。
安装:
dependencies:
go_router: ^13.0.0
基本用法:
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
);
// 在 MaterialApp 中使用
MaterialApp.router(
routerConfig: router,
);
导航:
// 导航到新页面
context.go('/details/123');
// 或者使用 push
context.push('/details/123');
// 返回
context.pop();
2. 嵌套路由
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
],
);
3. 路由守卫
final router = GoRouter(
redirect: (context, state) {
// 检查是否登录
final isLoggedIn = AuthService.isLoggedIn();
final isGoingToLogin = state.matchedLocation == '/login';
if (!isLoggedIn && !isGoingToLogin) {
return '/login';
}
if (isLoggedIn && isGoingToLogin) {
return '/';
}
return null; // 不重定向
},
routes: [
// ... 路由定义
],
);
导航栏和抽屉
1. BottomNavigationBar - 底部导航栏
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const SearchScreen(),
const ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '搜索',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
2. Drawer - 侧边抽屉
Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('菜单'),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('首页'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/');
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/settings');
},
),
],
),
),
body: const Center(child: Text('内容')),
);
3. TabBar - 标签页导航
class TabScreen extends StatelessWidget {
const TabScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('标签页'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: '首页'),
Tab(icon: Icon(Icons.search), text: '搜索'),
Tab(icon: Icon(Icons.person), text: '我的'),
],
),
),
body: const TabBarView(
children: [
HomeScreen(),
SearchScreen(),
ProfileScreen(),
],
),
),
);
}
}
深层链接(Deep Linking)
深层链接允许应用通过特定的 URL 直接打开特定的页面。
1. 使用 go_router 处理深层链接
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductScreen(id: id);
},
),
],
);
2. Android 配置
在 android/app/src/main/AndroidManifest.xml 中:
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
3. iOS 配置
在 ios/Runner/Info.plist 中:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
最佳实践
1. 路由管理最佳实践
- ✅ 使用 Router API:对于现代应用,推荐使用
go_router或类似的包 - ✅ 集中管理路由:将路由定义集中在一个文件中
- ✅ 使用类型安全的路由:避免字符串硬编码
- ✅ 处理错误路由:提供 404 页面
- ✅ 使用路由守卫:保护需要认证的页面
2. 导航最佳实践
- ✅ 使用 await 接收返回数据:确保正确处理异步返回
- ✅ 避免深层嵌套:限制导航堆栈的深度
- ✅ 提供返回按钮:确保用户可以返回
- ✅ 使用 WillPopScope:处理返回按钮的拦截
3. 代码组织
// routes/routes.dart - 集中管理路由
class AppRoutes {
static const String home = '/';
static const String details = '/details';
static const String settings = '/settings';
}
// routes/app_router.dart - 路由配置
final router = GoRouter(
routes: [
GoRoute(
path: AppRoutes.home,
builder: (context, state) => const HomeScreen(),
),
// ...
],
);
4. 拦截返回操作
class EditScreen extends StatelessWidget {
const EditScreen({super.key});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 显示确认对话框
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认'),
content: const Text('确定要离开吗?未保存的更改将丢失。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
return shouldPop ?? false;
},
child: Scaffold(
appBar: AppBar(title: const Text('编辑')),
body: const Center(child: Text('编辑内容')),
),
);
}
}
常见问题
1. Navigator 找不到 context
问题:Navigator operation requested with a context that does not include a Navigator
解决:确保 context 来自包含 MaterialApp 或 CupertinoApp 的 Widget 树。
// ❌ 错误
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) {
// 这里的 context 是正确的
return ElevatedButton(
onPressed: () {
Navigator.push(context, ...); // ✅
},
);
},
),
);
}
}
2. 路由参数类型转换错误
问题:type 'String' is not a subtype of type 'int'
解决:确保参数类型匹配,使用类型转换。
// 从路由参数获取时进行类型转换
final id = int.parse(state.pathParameters['id']!);
3. 返回数据为 null
问题:从新页面返回时,上一页接收到的数据为 null
解决:确保使用 await 等待返回结果。
// ✅ 正确
final result = await Navigator.push(...);
// ❌ 错误
final result = Navigator.push(...); // result 可能是 null
4. 路由堆栈过深
问题:导航堆栈过深导致内存问题
解决:使用 pushReplacement 或 pushAndRemoveUntil 替换路由。
// 替换当前路由
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
// 清除所有路由并导航到新页面
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
(route) => false, // 清除所有路由
);
总结
Flutter 的导航和路由系统提供了多种方式来管理页面跳转:
-
基本导航:使用
Navigator.push()和Navigator.pop() - 命名路由:适合简单的路由需求
- 路由生成器:提供更灵活的路由控制
- Router API:适合复杂应用和深层链接
选择合适的导航方式取决于你的应用需求:
- 简单应用:使用基本导航或命名路由
- 中等复杂度:使用路由生成器
- 复杂应用:使用 Router API(如 go_router)