🧭 导航组件详解
导航是应用的核心功能,Flutter 提供了强大的导航系统。本教程将详细介绍 Navigator、Router 以及页面跳转的最佳实践。
💡 重要提示:Flutter 3.28+ 引入了 Navigator 2.0,提供了更强大的路由管理和状态管理能力。
🔬 导航原理
Flutter 的导航系统基于路由栈(Route Stack)模型,理解其工作原理对于掌握页面跳转至关重要。
📚 路由栈机制
Navigator 内部维护一个路由栈,遵循后进先出(LIFO)原则:
- push:将新路由压入栈顶,当前页面被覆盖
- pop:将栈顶路由弹出,返回上一页
- pushReplacement:弹出栈顶路由,再压入新路由
- pushAndRemoveUntil:清空栈中所有路由,再压入新路由
📊 路由栈工作流程
← pop() 弹出栈顶
↑
页面 C(栈顶)
页面 B
页面 A(栈底/首页)
↓
→ push() 压入新页面
🔄 页面切换原理
push 操作
新页面入栈 → 触发 build → 执行动画 → 显示新页面
pop 操作
当前页面出栈 → 触发 dispose → 执行动画 → 显示上一页
replace 操作
先 pop 再 push → 原子操作 → 无返回动画
📞 参数传递原理
正向传递
通过构造函数或 settings.arguments 传递数据给新页面
反向返回
通过 pop(result) 返回数据,await push 接收结果
🎭 动画原理
页面切换动画由PageRouteBuilder的 transitionsBuilder 控制:
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// animation: 当前页面的动画控制器(0.0 - 1.0)
// secondaryAnimation: 下一页进入时的动画控制器
// child: 要显示的页面 Widget
var tween = Tween(begin: Offset(1.0, 0.0), end: Offset.zero);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
)
💡 关键点
- animation 值从 0.0 到 1.0:表示当前页面从开始到结束的动画进度
- secondaryAnimation:当新页面推入时,当前页面的退出动画
- child 参数:已经构建好的页面 Widget,直接包装动画即可
🔧 2. 路由构造函数
使用 onGenerateRoute 实现更灵活的路由管理。
📐 onGenerateRoute 路由匹配流程
settings.name = '/'
↓
return MaterialPageRoute(HomePage)
switch 语句匹配路由 → 返回对应的 MaterialPageRoute
MaterialApp(
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const HomePage());
case '/details':
final id = settings.arguments as int;
return MaterialPageRoute(
builder: (_) => DetailsPage(id: id),
);
default:
return MaterialPageRoute(
builder: (_) => const NotFoundPage(),
);
}
},
);
📑 3. TabBar 和 BottomNavigationBar
实现应用内的标签导航。
📐 TabBar 渲染效果
顶部标签切换,内容区域联动
TabBar 示例
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TabBar 示例'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.home), text: '首页'),
Tab(icon: Icon(Icons.search), text: '搜索'),
Tab(icon: Icon(Icons.person), text: '我的'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
HomePage(),
SearchPage(),
ProfilePage(),
],
),
);
}
}
📐 BottomNavigationBar 渲染效果
底部标签切换,当前项高亮显示
BottomNavigationBar 示例
class BottomNavExample extends StatefulWidget {
const BottomNavExample({super.key});
@override
State createState() => _BottomNavExampleState();
}
class _BottomNavExampleState extends State {
int _currentIndex = 0;
final List _pages = [
const HomePage(),
const SearchPage(),
const ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_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: '我的',
),
],
),
);
}
}
🚪 4. Drawer 侧边导航
实现应用内的侧边抽屉导航。
📐 Drawer 渲染效果
从屏幕左侧滑出,包含用户信息和导航菜单
Scaffold(
appBar: AppBar(
title: const Text('Drawer 示例'),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
UserAccountsDrawerHeader(
accountName: const Text('用户名'),
accountEmail: const Text('user@example.com'),
currentAccountPicture: const CircleAvatar(
backgroundImage: NetworkImage('https://via.placeholder.com/150'),
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
),
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');
},
),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('退出登录'),
onTap: () {
Navigator.pop(context);
// 退出登录逻辑
},
),
],
),
),
body: const Center(
child: Text('从左侧滑出 Drawer'),
),
);
✨ 5. Hero 动画
在页面间实现共享元素的过渡动画。
📐 Hero 动画效果
列表页
图片
产品标题
→
详情页
图片
产品标题
产品描述...
相同 tag 的元素在页面切换时产生平滑过渡动画
// 列表页面
Hero(
tag: 'product_image',
child: Image.network(
'https://example.com/product.jpg',
width: 100,
height: 100,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
},
)
// 详情页面
Scaffold(
body: Hero(
tag: 'product_image',
child: Image.network(
'https://example.com/product.jpg',
width: double.infinity,
fit: BoxFit.cover,
),
),
)
技巧:确保 Hero 组件的 tag 在两个页面中完全相同。
🎬 6. 页面过渡动画
自定义页面切换的过渡动画。
📐 页面过渡动画类型
SlideTransition (滑动)
页面A
页面B →
FadeTransition (淡入淡出)
页面A
页面B
ScaleTransition (缩放)
页面B
RotationTransition (旋转)
页面B
使用 PageRouteBuilder 的 transitionsBuilder 自定义动画
// 自定义页面过渡
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const 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,
);
},
transitionDuration: const Duration(milliseconds: 300),
),
);
// 淡入淡出效果
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
🔀 页面导航流程图
当前页面
↓
导航方式?
Navigator.push
↓
新页面入栈
Navigator.pop
↓
当前页面出栈
Navigator.pushReplacement
↓
替换当前页面
↓
完成导航
📊 导航方式对比
| 方法 | 栈操作 | 返回值 | 使用场景 |
|---|---|---|---|
push |
入栈 | 有 | 打开新页面 |
pop |
出栈 | 有 | 返回上一页 |
pushReplacement |
替换 | 无 | 替换当前页 |
pushAndRemoveUntil |
清空 | 无 | 登录后清空栈 |
pushNamed |
入栈 | 有 | 命名路由 |
📋 导航最佳实践
- 使用命名路由:集中管理路由,避免硬编码页面路径
- 路由常量化:将路由路径定义为常量,避免字符串拼写错误
- 参数类型安全:使用数据模型类传递复杂参数,而不是 Map
- 错误处理:为未知路由定义 404 页面
- 页面缓存:使用 AutomaticKeepAliveClientMixin 保持页面状态
- 转场动画:根据平台选择合适的 PageRoute 类型
- 路由测试:编写单元测试验证路由跳转逻辑
- 性能优化:使用 const 构造函数减少重建
🎯 下一步:掌握导航组件后,继续学习对话框组件。