09_路由与导航
Flutter 路由管理和页面导航
Flutter 的路由基于栈结构,Navigator 对应 Vue Router / React Router。
基本概念
Flutter 的路由 = 页面栈。push 进入新页面,pop 返回上一页,类似浏览器的前进后退。
基本导航
// push:跳转到新页面(类似 router.push('/detail'))
Navigator.of(context).push(
MaterialPageRoute( // Android 风格:从下向上滑入
builder: (context) => DetailPage(id: 123),
),
);
// CupertinoPageRoute:iOS 风格:从右向左滑入
Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => DetailPage(id: 123)),
);
// pop:返回上一页(类似 router.back())
Navigator.of(context).pop();
// pop 并携带返回值
Navigator.of(context).pop({'result': 'success'});
// 接收 pop 的返回值(await push)
final result = await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EditPage()),
);
if (result != null && mounted) {
print('编辑结果: $result');
}
常用 Navigator 方法
// pushReplacement:替换当前页(不能返回,类似 router.replace)
// 登录成功跳首页,不允许返回登录页
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomePage()),
);
// pushAndRemoveUntil:清空栈再跳转(退出登录 → 登录页)
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false, // false = 清空所有历史
);
// popUntil:连续返回到指定页面
Navigator.of(context).popUntil(ModalRoute.withName('/home'));
// canPop:是否可返回(自定义返回逻辑)
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
传参方式
方式一:构造函数传参(推荐,简单场景)
// 直接传给目标页构造函数
Navigator.push(context, MaterialPageRoute(
builder: (_) => ProductDetailPage(productId: 101, name: '商品A'),
));
// 目标页接收
class ProductDetailPage extends StatelessWidget {
final int productId;
final String name;
const ProductDetailPage({super.key, required this.productId, required this.name});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(name)),
body: Center(child: Text('ID: $productId')),
);
}
}
方式二:pop 返回数据
// 选择页面:选中后返回结果
class SelectCityPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: ['北京', '上海', '广州'].map((city) =>
ListTile(
title: Text(city),
onTap: () => Navigator.of(context).pop(city), // 携带数据返回
),
).toList(),
);
}
}
// 调用方接收结果
Future<void> _selectCity() async {
final city = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SelectCityPage()),
);
if (city != null && mounted) {
setState(() => _selectedCity = city);
}
}
命名路由(集中管理)
适合中大型 App,类似 Vue Router 的 routes 配置:
// main.dart
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/login': (context) => const LoginPage(),
'/profile': (context) => const ProfilePage(),
},
// 动态路由 + 未找到路由兜底
onGenerateRoute: (settings) {
// 解析动态路径,如 /product/123
final uri = Uri.parse(settings.name ?? '');
if (uri.pathSegments.first == 'product') {
final id = int.tryParse(uri.pathSegments.last);
if (id != null) {
return MaterialPageRoute(
builder: (_) => ProductDetailPage(productId: id, name: '商品'),
);
}
}
return MaterialPageRoute(builder: (_) => const NotFoundPage());
},
)
// 跳转(类似 router.push('/login'))
Navigator.of(context).pushNamed('/login');
Navigator.of(context).pushReplacementNamed('/home');
// 带参数(arguments = query params)
Navigator.of(context).pushNamed(
'/profile',
arguments: {'userId': 123, 'name': 'Alice'},
);
// 目标页接收
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final userId = args['userId'];
自定义路由动画
// 淡入淡出
Navigator.of(context).push(PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, _) => const NextPage(),
transitionsBuilder: (context, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
));
// 从右滑入(iOS 风格)
transitionsBuilder: (context, animation, _, child) {
final tween = Tween(begin: const Offset(1.0, 0.0), end: Offset.zero)
.chain(CurveTween(curve: Curves.easeInOut));
return SlideTransition(position: animation.drive(tween), child: child);
},
底部导航栏 Tab 架构
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
// IndexedStack 保留各 Tab 的状态(类似 Vue keep-alive)
final List<Widget> _pages = [
const HomePage(),
const SearchPage(),
const CartPage(),
const ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _currentIndex, children: _pages),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed, // 4 项以上必须设置
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
onTap: (index) => setState(() => _currentIndex = index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '购物车'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
}
IndexedStack vs PageView:
IndexedStack:所有 Tab 同时在树中,切换不重建(类似keep-alive),内存占用稍高PageView:懒加载,切换时重建(默认),支持手势左右滑动
WillPopScope(拦截返回键)
// 拦截 Android 物理返回键(或 iOS 侧滑返回)
WillPopScope(
onWillPop: () async {
// 返回 true = 允许返回;false = 阻止返回
if (_hasUnsavedChanges) {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => 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 confirmed ?? false;
}
return true;
},
child: Scaffold(...),
)