第10讲:导航与路由:在页面间穿梭
掌握页面导航技术,构建真正的多页面Flutter应用。
你好,欢迎回到《Flutter入门到精通》专栏。在上一讲中,我们学习了如何使用ListView和GridView展示动态数据。现在,让我们来学习如何将多个页面连接起来,构建一个完整的、可以导航的多页面应用。
一、导航基础:Navigator 与 Route
在Flutter中,导航由两个核心类管理:
Navigator:管理一组Route对象的Widget
Route:表示应用中的一个"屏幕"或"页面"
你可以把Navigator想象成一个栈管理器,Route就是栈中的页面。
1.1 基础导航方法
跳转到新页面
dart
Navigator.push(context,MaterialPageRoute(builder: (context) => NewPage()), );
返回上一页
dart
Navigator.pop(context);
带返回值的导航
dart
// 在第一个页面中跳转并等待返回值 final result = await Navigator.push(context,MaterialPageRoute(builder: (context) => SelectionPage()), );// 在第二个页面中返回数据 Navigator.pop(context, '选中的数据');
二、静态路由:基本导航操作
2.1 基础跳转示例
让我们创建一个简单的多页面应用来演示基本导航:
main.dart
dart
void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(title: '导航演示',theme: ThemeData(primarySwatch: Colors.blue,),home: const HomePage(), // 应用启动页面);}
}home_page.dart
dart
class HomePage extends StatelessWidget {const HomePage({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('首页'),backgroundColor: Colors.blue,),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [ElevatedButton(onPressed: () {// 跳转到详情页Navigator.push(context,MaterialPageRoute(builder: (context) => const DetailPage(title: '来自首页的详情',content: '这是从首页传递过来的内容。',),),);},child: const Text('跳转到详情页'),),const SizedBox(height: 20),ElevatedButton(onPressed: () {// 跳转到设置页Navigator.push(context,MaterialPageRoute(builder: (context) => const SettingsPage(),),);},child: const Text('跳转到设置页'),),],),),);}
}detail_page.dart
dart
class DetailPage extends StatelessWidget {final String title;final String content;const DetailPage({super.key,required this.title,required this.content,});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(title),backgroundColor: Colors.green,),body: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text(content,style: const TextStyle(fontSize: 18),),const SizedBox(height: 20),ElevatedButton(onPressed: () {// 返回上一页Navigator.pop(context);},child: const Text('返回'),),],),),);}
}settings_page.dart
dart
class SettingsPage extends StatelessWidget {const SettingsPage({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('设置'),backgroundColor: Colors.orange,),body: ListView(children: [ListTile(leading: const Icon(Icons.person),title: const Text('个人资料'),onTap: () {// 跳转到个人资料页Navigator.push(context,MaterialPageRoute(builder: (context) => const ProfilePage(),),);},),ListTile(leading: const Icon(Icons.notifications),title: const Text('通知设置'),onTap: () {// 跳转到通知设置页Navigator.push(context,MaterialPageRoute(builder: (context) => const NotificationSettingsPage(),),);},),ListTile(leading: const Icon(Icons.security),title: const Text('隐私设置'),onTap: () {// 跳转到隐私设置页Navigator.push(context,MaterialPageRoute(builder: (context) => const PrivacySettingsPage(),),);},),],),);}
}2.2 带返回值的导航
这是一个非常实用的功能,比如从列表中选择一项:
selection_page.dart
dart
class SelectionPage extends StatefulWidget {const SelectionPage({super.key});@overrideState<SelectionPage> createState() => _SelectionPageState();
}class _SelectionPageState extends State<SelectionPage> {String? _selectedItem;@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('选择项目'),),body: ListView(children: [_buildSelectionItem('选项 A', 'A'),_buildSelectionItem('选项 B', 'B'),_buildSelectionItem('选项 C', 'C'),_buildSelectionItem('选项 D', 'D'),],),floatingActionButton: FloatingActionButton(onPressed: _selectedItem != null? () {// 返回选中的值Navigator.pop(context, _selectedItem);}: null,child: const Icon(Icons.check),),);}Widget _buildSelectionItem(String title, String value) {return Card(margin: const EdgeInsets.all(8),color: _selectedItem == value ? Colors.blue[50] : null,child: ListTile(title: Text(title),leading: Radio<String>(value: value,groupValue: _selectedItem,onChanged: (String? value) {setState(() {_selectedItem = value;});},),onTap: () {setState(() {_selectedItem = value;});},),);}
}在首页中使用:
dart
ElevatedButton(onPressed: () async {// 等待选择页面返回结果final selectedValue = await Navigator.push(context,MaterialPageRoute(builder: (context) => const SelectionPage()),);// 显示选择结果if (selectedValue != null) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('你选择了: $selectedValue')),);}},child: const Text('打开选择页面'),
),三、命名路由:更清晰的导航管理
对于复杂的应用,使用命名路由可以让导航逻辑更清晰、易于维护。
3.1 配置命名路由
在MaterialApp中配置路由表:
main.dart
dart
class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(title: '命名路由演示',theme: ThemeData(primarySwatch: Colors.blue,),// 定义应用的初始路由initialRoute: '/',// 配置路由表routes: {'/': (context) => const HomePage(),'/detail': (context) => const DetailPage(title: '默认标题',content: '默认内容',),'/settings': (context) => const SettingsPage(),'/profile': (context) => const ProfilePage(),'/notifications': (context) => const NotificationSettingsPage(),'/privacy': (context) => const PrivacySettingsPage(),},);}
}3.2 使用命名路由导航
dart
// 简单的命名路由跳转
Navigator.pushNamed(context, '/settings');// 带参数的命名路由跳转
Navigator.pushNamed(context,'/detail',arguments: {'title': '动态标题','content': '动态内容',},
);3.3 在路由页面中接收参数
修改DetailPage来接收命名路由的参数:
dart
class DetailPage extends StatelessWidget {const DetailPage({super.key});@overrideWidget build(BuildContext context) {// 从路由参数中获取数据final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;final title = args?['title'] ?? '默认标题';final content = args?['content'] ?? '默认内容';return Scaffold(appBar: AppBar(title: Text(title),),body: Padding(padding: const EdgeInsets.all(16.0),child: Column(children: [Text(content),ElevatedButton(onPressed: () => Navigator.pop(context),child: const Text('返回'),),],),),);}
}3.4 更优雅的参数传递
创建一个路由参数类来管理参数:
route_arguments.dart
dart
// 详情页参数
class DetailPageArguments {final String title;final String content;DetailPageArguments({required this.title,required this.content,});
}// 个人资料页参数
class ProfilePageArguments {final String userId;final bool isEditable;ProfilePageArguments({required this.userId,this.isEditable = false,});
}使用类型安全的参数:
dart
// 传递参数
Navigator.pushNamed(context,'/detail',arguments: DetailPageArguments(title: '类型安全的标题',content: '类型安全的内容',),
);// 接收参数
class DetailPage extends StatelessWidget {const DetailPage({super.key});@overrideWidget build(BuildContext context) {final args = ModalRoute.of(context)!.settings.arguments as DetailPageArguments;return Scaffold(appBar: AppBar(title: Text(args.title)),body: Text(args.content),);}
}四、导航高级技巧
4.1 清除导航栈
有时候我们需要跳转到新页面时清除所有历史记录:
dart
// 跳转到新页面并清除所有历史记录(用户无法返回) Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const LoginPage()),(route) => false, // 移除所有现有路由 );// 跳转到新页面并保留首页 Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const MainPage()),(route) => route.isFirst, // 只保留第一个路由(首页) );
4.2 替换当前路由
用新页面替换当前页面:
dart
Navigator.pushReplacement(context,MaterialPageRoute(builder: (context) => const NewPage()), );// 命名路由版本 Navigator.pushReplacementNamed(context, '/newpage');
4.3 弹出到指定页面
返回到导航栈中的特定页面:
dart
// 弹出直到找到首页
Navigator.popUntil(context, (route) => route.isFirst);// 弹出直到找到指定名称的路由
Navigator.popUntil(context, ModalRoute.withName('/home'));五、综合实战:完整的用户流程
让我们创建一个完整的用户认证流程来演示复杂的导航场景:
auth_flow_example.dart
dart
class AuthFlowExample extends StatelessWidget {const AuthFlowExample({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(title: '用户认证流程',theme: ThemeData(primarySwatch: Colors.blue),initialRoute: '/',routes: {'/': (context) => const SplashPage(),'/login': (context) => const LoginPage(),'/register': (context) => const RegisterPage(),'/forgot-password': (context) => const ForgotPasswordPage(),'/home': (context) => const MainHomePage(),'/profile': (context) => const UserProfilePage(),},);}
}class SplashPage extends StatefulWidget {const SplashPage({super.key});@overrideState<SplashPage> createState() => _SplashPageState();
}class _SplashPageState extends State<SplashPage> {@overridevoid initState() {super.initState();_checkAuthStatus();}void _checkAuthStatus() async {// 模拟检查用户登录状态await Future.delayed(const Duration(seconds: 2));final isLoggedIn = false; // 这里应该是实际的认证检查if (mounted) {if (isLoggedIn) {// 已登录,跳转到首页并清除启动页Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const MainHomePage()),(route) => false,);} else {// 未登录,跳转到登录页并清除启动页Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const LoginPage()),(route) => false,);}}}@overrideWidget build(BuildContext context) {return Scaffold(backgroundColor: Colors.blue,body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [const CircularProgressIndicator(color: Colors.white),const SizedBox(height: 20),Text('加载中...',style: TextStyle(color: Colors.white, fontSize: 16),),],),),);}
}class LoginPage extends StatelessWidget {const LoginPage({super.key});void _performLogin(BuildContext context) async {// 显示加载指示器showDialog(context: context,barrierDismissible: false,builder: (context) => const Center(child: CircularProgressIndicator()),);// 模拟登录过程await Future.delayed(const Duration(seconds: 2));// 关闭加载指示器Navigator.pop(context);// 登录成功,跳转到首页Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const MainHomePage()),(route) => false,);}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('登录')),body: Padding(padding: const EdgeInsets.all(16.0),child: Column(children: [TextField(decoration: const InputDecoration(labelText: '用户名')),const SizedBox(height: 16),TextField(decoration: const InputDecoration(labelText: '密码'), obscureText: true),const SizedBox(height: 32),ElevatedButton(onPressed: () => _performLogin(context),child: const Text('登录'),),const SizedBox(height: 16),TextButton(onPressed: () {Navigator.pushNamed(context, '/register');},child: const Text('还没有账号?立即注册'),),TextButton(onPressed: () {Navigator.pushNamed(context, '/forgot-password');},child: const Text('忘记密码?'),),],),),);}
}class MainHomePage extends StatelessWidget {const MainHomePage({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('首页'),actions: [IconButton(icon: const Icon(Icons.person),onPressed: () {Navigator.pushNamed(context, '/profile');},),],),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [const Text('欢迎来到主页!'),const SizedBox(height: 20),ElevatedButton(onPressed: () {// 模拟退出登录Navigator.pushAndRemoveUntil(context,MaterialPageRoute(builder: (context) => const LoginPage()),(route) => false,);},child: const Text('退出登录'),),],),),);}
}导航最佳实践
保持导航简洁:避免过深的导航层级
使用命名路由:便于维护和团队协作
类型安全:为路由参数创建专门的类
错误处理:处理导航失败的情况
用户体验:提供清晰的导航反馈
结语
恭喜!通过本讲的学习,你已经掌握了Flutter中页面导航的核心技术。从基础跳转到复杂的命名路由,从简单传参到完整的用户流程,你现在可以构建真正的多页面应用了。
记住这些关键导航模式:
push/pop:基本的页面跳转和返回
命名路由:适合复杂应用的清晰结构
pushAndRemoveUntil:实现登录流程等场景
带返回值导航:用于选择和数据传递
在下一讲中,我们将进入Flutter开发的一个重要分水岭——状态管理。我们将深入学习StatefulWidget的状态管理,并介绍Provider等高级状态管理方案。
