导航和路由
导航是应用的核心功能之一。本章将介绍 Flutter 中页面跳转和路由管理的各种方式。
基本导航
Navigator.push
使用 Navigator.push 跳转到新页面:
// 定义页面
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 跳转到新页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
);
},
child: Text('跳转到第二页'),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('第二页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页
Navigator.pop(context);
},
child: Text('返回'),
),
),
);
}
}
传递参数
通过构造函数传递
// 发送页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(id: 123, title: '详情'),
),
);
// 接收页面
class DetailPage extends StatelessWidget {
final int id;
final String title;
const DetailPage({Key? key, required this.id, required this.title})
: super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('ID: $id')),
);
}
}
返回数据
// 发送页面等待返回结果
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectionPage(),
),
);
if (result != null) {
print('选择了:$result');
}
},
child: Text('选择项目'),
),
),
);
}
}
// 返回数据的页面
class SelectionPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('选择')),
body: ListView(
children: ['选项A', '选项B', '选项C'].map((item) {
return ListTile(
title: Text(item),
onTap: () {
Navigator.pop(context, item); // 返回选中的数据
},
);
}).toList(),
),
);
}
}
命名路由
定义路由
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
);
}
}
// 使用命名路由跳转
Navigator.pushNamed(context, '/detail');
// 传递参数
Navigator.pushNamed(
context,
'/detail',
arguments: {'id': 123, 'title': '详情'},
);
// 在目标页面获取参数
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return Scaffold(
appBar: AppBar(title: Text(args['title'])),
body: Center(child: Text('ID: ${args['id']}')),
);
}
}
onGenerateRoute
使用 onGenerateRoute 可以更灵活地处理路由:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
},
onGenerateRoute: (settings) {
if (settings.name == '/detail') {
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (context) => DetailPage(
id: args['id'],
title: args['title'],
),
);
}
return null;
},
);
}
}
GoRouter(推荐)
GoRouter 是 Flutter 官方团队维护的路由库,支持声明式路由、深层链接等高级功能。
添加依赖
dependencies:
go_router: ^12.0.0
基本用法
import 'package:go_router/go_router.dart';
// 1. 定义路由
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/detail/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsPage(),
),
],
);
// 2. 配置应用
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
// 3. 导航
context.go('/detail/123'); // 替换当前页面
context.push('/detail/123'); // 推入新页面
context.pop(); // 返回
context.go('/'); // 返回首页(清空栈)
// 4. 获取参数
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
final name = state.uri.queryParameters['name'] ?? '未知';
return UserPage(id: id, name: name);
},
);
// 使用
context.go('/user/123?name=张三');
嵌套路由
final _router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
],
),
GoRoute(
path: '/search',
builder: (context, state) => SearchPage(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfilePage(),
),
],
),
],
);
// 底部导航栏
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _calculateSelectedIndex(context),
onTap: (index) => _onItemTapped(index, context),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/search')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/search');
break;
case 2:
context.go('/profile');
break;
}
}
}
路由守卫
final _router = GoRouter(
redirect: (context, state) {
// 检查是否登录
final isLoggedIn = context.read<AuthProvider>().isLoggedIn;
final isLoggingIn = state.uri.toString() == '/login';
// 未登录且不在登录页,重定向到登录页
if (!isLoggedIn && !isLoggingIn) {
return '/login';
}
// 已登录且在登录页,重定向到首页
if (isLoggedIn && isLoggingIn) {
return '/';
}
return null; // 不重定向
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => LoginPage(),
),
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
],
);
页面转场动画
内置转场动画
// Fade 转场
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
// Slide 转场
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.ease;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
// Scale 转场
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
),
child: child,
);
},
),
);
Hero 动画
Hero 动画可以在页面切换时创建平滑的共享元素过渡:
// 源页面
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
},
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/200',
width: 100,
height: 100,
),
),
),
);
}
}
// 目标页面
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Hero(
tag: 'image-hero', // 相同的 tag
child: Image.network(
'https://picsum.photos/400',
width: 300,
height: 300,
),
),
),
);
}
}
对话框路由
打开对话框
// AlertDialog
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('提示'),
content: Text('确定要删除吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('确定'),
),
],
);
},
).then((value) {
if (value == true) {
print('用户确认删除');
}
});
// SimpleDialog
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('选择颜色'),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'red'),
child: Text('红色'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'green'),
child: Text('绿色'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'blue'),
child: Text('蓝色'),
),
],
);
},
).then((color) {
if (color != null) {
print('选择了:$color');
}
});
// BottomSheet
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
height: 200,
child: Column(
children: [
ListTile(
leading: Icon(Icons.photo),
title: Text('从相册选择'),
onTap: () {
Navigator.pop(context);
print('从相册选择');
},
),
ListTile(
leading: Icon(Icons.camera),
title: Text('拍照'),
onTap: () {
Navigator.pop(context);
print('拍照');
},
),
],
),
);
},
);
小结
本章我们学习了:
- 基本导航:Navigator.push 和 Navigator.pop
- 参数传递:构造函数传参、返回数据
- 命名路由:routes、onGenerateRoute
- GoRouter:声明式路由、嵌套路由、路由守卫
- 转场动画:Fade、Slide、Scale、Hero
- 对话框路由:AlertDialog、BottomSheet
练习
- 实现一个多页面表单,使用 Navigator 传递数据
- 使用 GoRouter 实现底部导航栏应用
- 创建自定义的页面转场动画
- 实现 Hero 动画的图片浏览器
下一步
下一章我们将学习 网络请求,掌握如何在 Flutter 中进行网络通信。