Flutter Navigator 1.0命令式导航—路由栈管理与页面传参|新宇宙博客Back to listFlutter Navigator 1.0 与命令式导航深度解析
Site Owner
Published on 2026-05-22
系统讲解Flutter Navigator push/pop操作、命名路由配置、onGenerateRoute、页面传参及WillPopScope拦截

Flutter Navigator 1.0 与命令式导航深度解析 (Imperative Navigation)
前言
Navigator 1.0 是 Flutter 最原始的导航 API,基于命令式的栈操作(push/pop)。虽然在 Web 应用和复杂导航场景中有一定局限性,但对于移动端 App 来说,掌握 Navigator 1.0 是理解 Flutter 导航体系的必要基础,许多中小型 App 至今仍在使用它。
一、Navigator 的栈模型
Navigator 维护了一个 Route 栈,遵循后进先出(LIFO)原则:
底部 顶部(当前页面)
┌──────────────────────────────────────────────┐
│ HomePage │ ProductListPage │ ProductDetailPage│ ← 当前可见
└──────────────────────────────────────────────┘
← pop() push() →
二、基础导航操作
2.1 push:跳转到新页面
// 方式一:MaterialPageRoute(Material 动画)
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProductDetailPage(productId: '123'),
),
);
// 方式二:命名路由(需要提前注册)
Navigator.of(context).pushNamed('/product/detail', arguments: {'id': '123'});
// 方式三:简写
Navigator.push(context, MaterialPageRoute(builder: (_) => const DetailPage()));
2.2 pop:返回上一页
// 简单返回
Navigator.of(context).pop();
// 返回并传递数据给上一页
Navigator.of(context).pop({'result': 'success', 'data': someData});
// 接收返回值(在 push 的地方 await)
final result = await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EditPage()),
);
// result 就是 EditPage 中 pop 时传递的数据
if (result != null) {
print('编辑结果:$result');
}
2.3 replace 系列
// pushReplacement:替换当前页面(登录成功后跳首页,不能返回登录页)
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomePage()),
);
// pushAndRemoveUntil:清空栈并跳转(退出登录 → 清空所有页面 → 跳登录页)
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false, // 移除所有路由
);
// pushAndRemoveUntil 保留第一个页面
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NewPage()),
ModalRoute.withName('/'), // 保留 '/' 路由(首页)
);
// popUntil:连续 pop 到指定路由
Navigator.of(context).popUntil(ModalRoute.withName('/home'));
三、命名路由
命名路由通过字符串路径管理导航,适合简单的路由结构:
void main() {
runApp(
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/products': (context) => const ProductListPage(),
'/settings': (context) => const SettingsPage(),
// 注意:命名路由不支持动态参数(如 /product/123)
// 动态参数需要通过 arguments 传递
'/product/detail': (context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return ProductDetailPage(productId: args['id']);
},
},
// 处理未知路由
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (_) => const NotFoundPage(),
);
},
// 动态路由生成(比 routes 更灵活)
onGenerateRoute: (settings) {
// 可以在这里处理动态路径
if (settings.name?.startsWith('/product/') == true) {
final id = settings.name!.split('/').last;
return MaterialPageRoute(
builder: (_) => ProductDetailPage(productId: id),
);
}
return null; // 交给 routes 或 onUnknownRoute 处理
},
),
);
}
四、自定义页面转场动画
// 自定义 PageRoute
class SlideFromRightRoute<T> extends PageRouteBuilder<T> {
final Widget page;
SlideFromRightRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 从右侧滑入
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
final tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
// 从底部弹出(模态框效果)
class BottomSlideRoute<T> extends PageRouteBuilder<T> {
final Widget page;
BottomSlideRoute({required this.page})
: super(
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final tween = Tween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.decelerate));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
opaque: false, // 背景透明
barrierColor: Colors.black54, // 遮罩颜色
barrierDismissible: true, // 点击遮罩关闭
);
}
// 使用
Navigator.of(context).push(SlideFromRightRoute(page: const DetailPage()));
Navigator.of(context).push(BottomSlideRoute(page: const OptionsSheet()));
五、Hero 动画:跨页面共享元素
// 列表页:图片带 Hero 标签
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ProductDetailPage(product: product),
),
),
child: Hero(
tag: 'product-image-${product.id}', // 唯一 tag
child: Image.network(product.imageUrl),
),
);
}
}
// 详情页:使用相同 tag
class ProductDetailPage extends StatelessWidget {
final Product product;
const ProductDetailPage({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Hero(
tag: 'product-image-${product.id}', // 必须与来源页面一致
child: Image.network(product.imageUrl),
),
Text(product.name),
],
),
);
}
}
自定义 Hero 飞行效果
Hero(
tag: 'avatar',
// 飞行过程中显示的 Widget(默认是目标 Widget)
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (_, __) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle, // 飞行时显示为圆形
color: Colors.blue.withOpacity(animation.value),
),
child: const FlutterLogo(),
),
);
},
child: const FlutterLogo(size: 50),
)
六、WillPopScope / PopScope:拦截返回操作
// Flutter 3.12 以前:WillPopScope(已废弃,但仍可用)
WillPopScope(
onWillPop: () async {
// 返回 true 允许返回,false 阻止返回
final shouldPop = 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 shouldPop ?? false;
},
child: Scaffold(/* ... */),
)
// Flutter 3.12+:PopScope(推荐)
PopScope(
canPop: false, // 禁止直接 pop
onPopInvoked: (didPop) async {
if (didPop) return; // 已经 pop 了,不再处理
final shouldPop = await showDialog<bool>(/* 同上 */);
if (shouldPop == true && context.mounted) {
Navigator.of(context).pop();
}
},
child: Scaffold(/* ... */),
)
七、嵌套 Navigator:独立导航栈
底部导航栏场景:每个 Tab 维护独立的导航栈(切换 Tab 不会影响其他 Tab 的页面状态)。
class MainPage extends StatefulWidget {
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
// 每个 Tab 一个 GlobalKey,持有对应 Navigator 的引用
final List<GlobalKey<NavigatorState>> _navigatorKeys = [
GlobalKey<NavigatorState>(),
GlobalKey<NavigatorState>(),
GlobalKey<NavigatorState>(),
];
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 优先让当前 Tab 的 Navigator 处理返回
final currentNavigator = _navigatorKeys[_currentIndex].currentState!;
if (currentNavigator.canPop()) {
currentNavigator.pop();
return false; // 阻止系统返回
}
return true; // 允许退出 App
},
child: Scaffold(
body: IndexedStack(
// IndexedStack 保持每个 Tab 的状态
index: _currentIndex,
children: [
_TabNavigator(navigatorKey: _navigatorKeys[0], initialRoute: '/home'),
_TabNavigator(navigatorKey: _navigatorKeys[1], initialRoute: '/explore'),
_TabNavigator(navigatorKey: _navigatorKeys[2], initialRoute: '/profile'),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: '发现'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
),
);
}
}
class _TabNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
final String initialRoute;
const _TabNavigator({required this.navigatorKey, required this.initialRoute});
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: initialRoute,
onGenerateRoute: (settings) {
// 每个 Tab 的路由表
switch (settings.name) {
case '/home':
return MaterialPageRoute(builder: (_) => const HomePage());
case '/home/detail':
return MaterialPageRoute(builder: (_) => const DetailPage());
default:
return MaterialPageRoute(builder: (_) => const NotFoundPage());
}
},
);
}
}
八、Navigator.of(context) 的查找机制
// Navigator.of(context) 的内部实现:
static NavigatorState of(BuildContext context, {bool rootNavigator = false}) {
NavigatorState? navigator;
if (rootNavigator) {
// 找到最顶层的 Navigator(通常是 MaterialApp 创建的那个)
navigator = context.findRootAncestorStateOfType<NavigatorState>();
} else {
// 找到最近的 Navigator(默认行为)
navigator = context.findAncestorStateOfType<NavigatorState>();
}
assert(navigator != null, '找不到 Navigator');
return navigator!;
}
// 在嵌套 Navigator 中,如果想在根 Navigator 上 push(全屏弹窗)
// 而不是当前 Tab 的 Navigator 上 push
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (_) => const GlobalOverlayPage()),
);
九、处理 Android 物理返回键
// Android 的物理返回键会触发 Navigator.maybePop()
// 如果当前栈只有一个页面,会调用 SystemNavigator.pop()(退出 App)
// 完全自定义返回键行为:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: GlobalKey<NavigatorState>(), // 保存根 Navigator 引用
// ...
);
}
}
// 如果需要双击返回退出 App:
class ExitConfirmWrapper extends StatefulWidget {
final Widget child;
const ExitConfirmWrapper({super.key, required this.child});
@override
State<ExitConfirmWrapper> createState() => _ExitConfirmWrapperState();
}
class _ExitConfirmWrapperState extends State<ExitConfirmWrapper> {
DateTime? _lastPressTime;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
final now = DateTime.now();
if (_lastPressTime == null ||
now.difference(_lastPressTime!) > const Duration(seconds: 2)) {
_lastPressTime = now;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('再按一次退出应用')),
);
} else {
SystemNavigator.pop(); // 退出 App
}
},
child: widget.child,
);
}
}
十、过关自检
Q:Navigator.of(context) 如何找到最近的 Navigator?
Navigator.of(context) 调用 context.findAncestorStateOfType<NavigatorState>(),这个方法从当前 Element 开始沿着 Element 树向上遍历,找到第一个类型为 NavigatorState 的祖先 State 并返回。这意味着在嵌套 Navigator 的场景下,它会找到最近的(最内层的)Navigator,而不是根 Navigator。如果要找根 Navigator,需要传入 rootNavigator: true,此时会调用 findRootAncestorStateOfType<NavigatorState>(),一直找到树的顶端。
Q:如何处理底部导航栏中多个 Tab 各自维护独立导航栈、Android 物理返回键正确处理多层 Navigator 的场景?
- 用
IndexedStack 保持每个 Tab 的 Widget 状态(不销毁)
- 为每个 Tab 的
Navigator 分配独立的 GlobalKey<NavigatorState>
- 在根
WillPopScope/PopScope 的回调中,先调用当前 Tab 的 Navigator.canPop() 和 Navigator.pop(),如果当前 Tab 无法再 pop(只有一级页面),才允许系统处理返回键(返回 true,可能退出 App)。见第七节完整实现。
小结
| API | 用途 |
|---|
push | 跳转到新页面,压栈 |
pop | 返回上一页,可携带数据 |
pushReplacement | 替换当前页面(如登录成功后跳首页) |
pushAndRemoveUntil | 跳转并清空部分/全部栈(退出登录) |
popUntil | 连续返回到指定页面 |
Hero | 跨页面共享元素动画 |
PopScope | 拦截返回操作(二次确认等) |
| 嵌套 Navigator | 每个 Tab 独立导航栈 |
rootNavigator: true | 在最顶层 Navigator 操作(全屏弹窗) |