什么是嵌套导航
普通导航
在 Flutter(或者 GetX)里,导航(Navigation) 就是我们在不同页面之间跳转的能力。
比如:
Get.to(PageA()); // 从首页跳到 A 页面
Get.to(PageB()); // 再从 A 跳到 B 页面
Get.back(); // 返回到 A
这时候整个应用只有 一个导航栈(navigation stack):
Home → A → B
嵌套导航
嵌套导航就是 在应用里存在多个独立的导航栈。
比如一个典型的 底部导航栏(BottomNavigationBar)App:
- Tab1(首页)
- Tab2(设置)
如果你在 Tab1 里继续点进去详情页:
Tab1 → Detail
然后切换到 Tab2:
Tab2
这时候 App 里同时存在两个导航栈:
栈1 (id=1): Tab1 → Detail
栈2 (id=2): Tab2
当你切换 Tab 时,每个 Tab 的页面历史记录都保留。
这就是 嵌套导航。
效果图
程序启动的时候,显示Tab1首页
:

点击进入详情页:这个时候底边栏还在

进入到Tab2
的页面

点击Tab1
,发现还是显示详情页,表面Tab1
的历史记录还在。否则会显示Tab 1 首页

Navigator1.0原生路由
代码案例
main方法和主页
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(title: 'Navigator 1.0 嵌套导航 Demo', home: MainPage());
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// body(主体内容区域)
// IndexedStack:一个特殊的 Stack,它会将所有子组件都保留在内存中,但只显示 index 对应的那一个。
// 好处:切换 Tab 时不会重建页面(状态会保留)。
// 不用 IndexedStack 的话,切换 Tab 时页面会被销毁重建。
body: IndexedStack(
// _index 控制显示哪个子页面。
// 一个整型变量,默认值是 0,表示当前激活的 Tab 页(默认显示第 0 个 Tab)。
index: _index,
// 这里的 Tab1Page 和 Tab2Page 会被保留在内存中
children: const [Tab1Page(), Tab2Page()],
// children:IndexedStack 的子组件列表。
// [Tab1Page(), Tab2Page()]:定义了两个 Tab 页。
// 因为用了 IndexedStack,即使当前只显示一个,另一个也不会销毁。
// const:优化性能,避免无意义的重复创建。
),
// bottomNavigationBar
// bottomNavigationBar:Scaffold 的底部导航区域,用来切换页面。
// BottomNavigationBar:Flutter 提供的底部 Tab 导航组件。
// currentIndex:当前选中的 Tab 索引(高亮显示)。
// 绑定 _index,保证导航栏和上面的 IndexedStack 一致。
// onTap:点击底部某个 Tab 时触发。
// 参数 i:系统自动传入的点击项的下标(0 代表第一个 Tab,1 代表第二个 Tab)。
// setState:修改 _index,并通知 Flutter 重新执行 build 方法。
// 整个逻辑:点击 Tab → _index 变了 → IndexedStack 重新渲染,显示新的页面 → 底部导航栏同步高亮。
// items:底部导航的按钮数组。
// 每个 BottomNavigationBarItem 代表一个 Tab。
// icon:显示的图标,这里用的是 Material 内置的 Icons.home、Icons.settings。
// label:文字标题(显示在图标下方)。
// const:同样是性能优化。
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Tab1"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Tab2"),
],
),
);
}
}
Tab1和Tab2
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
// Tab1Page 不是一个普通页面,而是 一个小型导航器(Navigator)。
return Navigator(
// onGenerateRoute 指定了子导航器的初始页面是 Tab1Home。
onGenerateRoute: (settings) {
return MaterialPageRoute(builder: (_) => const Tab1Home());
},
);
}
}
class Tab1Home extends StatelessWidget {
const Tab1Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab1 首页")),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const DetailPage()));
},
child: const Text("进入详情页"),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("详情页")),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("返回 Tab1"),
),
),
);
}
}
class Tab2Page extends StatelessWidget {
const Tab2Page({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab2 页面")),
body: const Center(child: Text("这里是 Tab2")),
);
}
}
Navigator
- 相当于创建了一个局部导航器(nested navigator)。
- 它有自己的路由栈,和
MainPage
外层的Navigator
互不干扰。 - 所以
Tab1Page
下的跳转不会影响外层的底部导航。
onGenerateRoute
- 这是 Navigator 第一次构建时会调用的函数。
- 它的任务就是告诉
Navigator
:“我初始要显示哪个页面?” - 在这里,它返回了一个
MaterialPageRoute
,表示默认显示Tab1Home
。
MaterialPageRoute
是 Flutter 提供的一个最常用的路由类,基于 Material Design 的页面切换。
它内部帮你封装了:
- 页面过渡动画(默认从右往左滑入,符合 Android 的习惯)。
- 页面生命周期管理。
它需要一个
builder
参数,这里写的是:builder: (_) => const Tab1Home()
表示:这个路由的内容就是
Tab1Home
页面。
Navigator.of(context).push
- 这里的
Navigator.of(context)
会找到 最近的 Navigator,也就是Tab1Page
里面的那个 嵌套 Navigator,而不是最外层的MaterialApp
Navigator。 - 因此,
DetailPage
只会在 Tab1 的导航栈里被 push,而不会影响主页面或者 Tab2。 - 切换到 Tab2 再切换回来时,
DetailPage
仍然存在,因为IndexedStack
保留了状态。
- 这里的
返回时
Navigator.of(context).pop()
- 同样是操作 子 Navigator 的栈,从
DetailPage
返回到Tab1Home
。 - 不会影响全局 Navigator,也不会跳回到
MainPage
。
- 同样是操作 子 Navigator 的栈,从
路由栈示意图
有子 Navigator
初始状态(进入应用)
主 Navigator (MaterialApp)
└── MainPage
├── Tab1Page
│ └── 子 Navigator (Tab1 专属)
│ └── Tab1Home (页面在这里)
│
└── Tab2Page
在 Tab1 中 push 到 DetailPage
主 Navigator (MaterialApp)
└── MainPage
├── Tab1Page
│ └── 子 Navigator (Tab1 专属)
│ ├── Tab1Home
│ └── DetailPage <-- push 进来的
│
└── Tab2Page
切换到 Tab2
主 Navigator (MaterialApp)
└── MainPage
├── Tab1Page (保持状态,不销毁)
│ └── 子 Navigator
│ ├── Tab1Home
│ └── DetailPage <-- 还在栈里
│
└── Tab2Page (现在显示出来)
从 DetailPage 返回 (pop)
主 Navigator (MaterialApp)
└── MainPage
├── Tab1Page
│ └── 子 Navigator
│ └── Tab1Home <-- 回到这里
│
└── Tab2Page
没有子 Navigator 的情况(直接 push 到主栈)
初始
主 Navigator
└── MainPage
├── Tab1Page
└── Tab2Page
在 Tab1 push 到 DetailPage
主 Navigator
└── MainPage
├── Tab1Page
└── Tab2Page
└── DetailPage <-- 跑到主栈里(不会有底边栏)
Navigator1.0 + GetX
不注册到全局路路由
完整代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// MaterialApp 换成 GetMaterialApp
return GetMaterialApp(
title: 'Navigator 1.0 + GetX 嵌套导航 Demo',
home: const MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("GetX+Navigator 1.0 嵌套导航 Demo")),
body: IndexedStack(
index: _index,
children: const [Tab1Page(), Tab2Page()],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Tab1"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Tab2"),
],
),
);
}
}
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
// 从 GetX 4.6.x 之后,GetNavigator 就逐渐和 Flutter 原生的 Navigator 2.0 完全对齐,
// 因此现在不再支持onGenerateRoute这个参数,如果想用只能通过Navigator
return Navigator(
key: Get.nestedKey(1),
onGenerateRoute: (settings) {
if (settings.name == "/tab1/detail") {
return MaterialPageRoute(builder: (_) => const DetailPage());
}
return MaterialPageRoute(builder: (_) => const Tab1Home());
},
);
}
}
class Tab1Home extends StatelessWidget {
const Tab1Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab1 首页")),
body: Center(
child: ElevatedButton(
onPressed: () {
// ✅ GetX 的跳转方式
Get.toNamed("/tab1/detail", id: 1);
// Get.to(
// () => const DetailPage(),
// id: 1, // 注意指定子导航器 ID
// );
// 这里的 id:1 是对应 Tab1Page 里的 Navigator
},
child: const Text("进入详情页"),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("详情页")),
body: Center(
child: ElevatedButton(
onPressed: () => Get.back(id: 1), // ✅ 返回 Tab1 的路由栈
child: const Text("返回 Tab1"),
),
),
);
}
}
class Tab2Page extends StatelessWidget {
const Tab2Page({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(2), // ✅ 指定子导航器 ID 为 2
onGenerateRoute: (settings) {
// 这里始终返回 Tab2 首页
return MaterialPageRoute(builder: (_) => const Tab2Home());
},
);
}
}
class Tab2Home extends StatelessWidget {
const Tab2Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab2 页面")),
body: const Center(child: Text("这里是 Tab2")),
);
}
}
关键代码截图
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
// 从 GetX 4.6.x 之后,GetNavigator 就逐渐和 Flutter 原生的 Navigator 2.0 完全对齐,
// 因此现在不再支持onGenerateRoute这个参数,如果想用只能通过Navigator
return Navigator(
key: Get.nestedKey(1),
onGenerateRoute: (settings) {
if (settings.name == "/tab1/detail") {
return MaterialPageRoute(builder: (_) => const DetailPage());
}
return MaterialPageRoute(builder: (_) => const Tab1Home());
},
);
}
}
// ✅ GetX 的跳转方式
Get.toNamed("/tab1/detail", id: 1);
// Get.to(
// () => const DetailPage(),
// id: 1, // 注意指定子导航器 ID
// );
// 这里的 id:1 是对应 Tab1Page 里的 Navigator
() => Get.back(id: 1), // ✅ 返回 Tab1 的路由栈
key: Get.nestedKey(1)
和id
Navigator(
key: Get.nestedKey(1),
onGenerateRoute: (settings) { ... }
);
这里你创建了一个 子 Navigator,并给它设置了一个唯一的 key
Get.nestedKey(1)
就是创建一个全局唯一的GlobalKey<NavigatorState>
,并且与id=1
绑定。- 这样你在其他地方就可以用
id:1
来操作这个 Navigator。
GetX 通过 id
来区分不同的子导航器。
Get.to(() => const DetailPage(), id: 1);
或者Get.toNamed("/tab1/detail", id: 1);
- 作用:导航到一个新页面,相当于
Navigator.push()
。id: 1
指定操作的是 id=1 的子导航器(也就是你在Tab1Page
里定义的GetNavigator
)。- 如果不写
id: 1
,页面会被加到根导航器(整个应用的栈),而不是 Tab1 的局部栈。 - 写了
id: 1
,就是往Tab1
的导航栈里push
页面。
- 如果不写
导航栈简图
初始状态(进入 App 后,默认在 Tab1)
全局路由栈 (root Navigator)
└── MainPage (底部 Tab 页面容器)
├── Tab1Page (子导航器 id=1)
│ └── /tab1/home (Tab1Home 页面)
│
└── Tab2Page (子导航器 id=2)
└── /tab2/home (Tab2Home 页面)
进入详情页
在 Tab1 中执行:Get.to(() => const DetailPage(), id: 1);
此时 Tab1 的子导航器栈 增加了一个页面:只影响 Tab1 的子导航器,不会改变全局 root 的栈。
全局路由栈 (root Navigator)
└── MainPage
├── Tab1Page (子导航器 id=1)
│ ├── /tab1/home (Tab1Home 页面)
│ └── /tab1/detail (DetailPage 页面) <-- 新压入
│
└── Tab2Page (子导航器 id=2)
└── /tab2/home (Tab2Home 页面)
如果不带id
,执行Get.to(() => const DetailPage());
全局路由栈 (root Navigator)
└── MainPage
├── Tab1Page (子导航器 id=1)
│ └── /tab1/home
│
└── Tab2Page (子导航器 id=2)
└── /tab2/home
└── DetailPage <-- 注意!这是在 root 上
这时,DetailPage
被 压入全局 root 栈,而不是 Tab1 的子导航器:
切换到 Tab2
全局路由栈 (root Navigator)
└── MainPage
├── Tab1Page (子导航器 id=1)
│ ├── /tab1/home
│ └── /tab1/detail (栈里依然保留)
│
└── Tab2Page (子导航器 id=2)
└── /tab2/home
注意:Tab2 只看到自己的 /tab2/home
,但 Tab1 的栈仍然保留,切回来时会直接看到 DetailPage
。
如果在 Tab1 点返回:
执行:Get.back(id: 1);
全局路由栈 (root Navigator)
└── MainPage
├── Tab1Page (子导航器 id=1)
│ └── /tab1/home
│
└── Tab2Page (子导航器 id=2)
└── /tab2/home
Tab1 的子导航器会 pop 出 /tab1/detail
,回到 home
:
主 Navigator
└── MainPage
├── Tab1Page
└── Tab2Page
└── DetailPage <-- 跑到主栈里(不会有底边栏)
Navigator2.0 + GetX
完整代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Navigator 2.0 + GetX 嵌套导航 Demo',
home: const MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("GetX + Navigator 2.0 嵌套导航")),
body: IndexedStack(
index: _index,
children: const [Tab1Page(), Tab2Page()],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Tab1"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Tab2"),
],
),
);
}
}
// ---------------- Tab1 ----------------
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
return GetNavigator(
key: Get.nestedKey(1), // 子导航器ID
pages: [
// 初始页面
const MaterialPage(name: "/tab1/home", child: Tab1Home()),
],
onPopPage: (route, result) => route.didPop(result),
);
}
}
class Tab1Home extends StatelessWidget {
const Tab1Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab1 首页")),
body: Center(
child: ElevatedButton(
onPressed: () {
// Navigator 2.0 风格 push 页面到 Tab1 的子导航器
final nav = Get.nestedKey(1)!.currentState!;
nav.push(MaterialPageRoute(builder: (_) => const DetailPage()));
// 或者用 GetX 提供的方式:
// Get.to(() => const DetailPage(), id: 1);
},
child: const Text("进入详情页"),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("详情页")),
body: Center(
child: ElevatedButton(
onPressed: () {
final nav = Get.nestedKey(1)!.currentState!;
nav.pop();
// 或者用 GetX:
// Get.back(id: 1);
},
child: const Text("返回 Tab1"),
),
),
);
}
}
// ---------------- Tab2 ----------------
class Tab2Page extends StatelessWidget {
const Tab2Page({super.key});
@override
Widget build(BuildContext context) {
return GetNavigator(
key: Get.nestedKey(2), // 子导航器ID
pages: [const MaterialPage(name: "/tab2/home", child: Tab2Home())],
onPopPage: (route, result) => route.didPop(result),
);
}
}
class Tab2Home extends StatelessWidget {
const Tab2Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab2 页面")),
body: const Center(child: Text("这里是 Tab2")),
);
}
}
关键代码解读
// 每个 Tab 都使用 GetNavigator(完全符合 Navigator 2.0 风格)
//使用 pages 参数而不是 onGenerateRoute
// 通过 id 指定子导航器
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
return GetNavigator(
key: Get.nestedKey(1), // 子导航器ID
pages: [
// 初始页面
const MaterialPage(name: "/tab1/home", child: Tab1Home()),
],
onPopPage: (route, result) => route.didPop(result),
);
}
}
// 通过 id 指定子导航器
// Navigator 2.0 风格 push 页面到 Tab1 的子导航器
final nav = Get.nestedKey(1)!.currentState!;
nav.push(
MaterialPageRoute(builder: (_) => const DetailPage()),
);
// 或者用 GetX 提供的方式:
// Get.to(() => const DetailPage(), id: 1);
final nav = Get.nestedKey(1)!.currentState!;
nav.pop();
// 或者用 GetX:
// Get.back(id: 1);
GetNavigator
- 相当于一个 局部的 Navigator,它维护自己的路由栈。
- 可以理解为
Navigator
的 GetX 版本。
key: Get.nestedKey(1)
- 为这个局部 Navigator 指定一个唯一标识
1
。 - 这个 key 可以让我们在其他地方通过
Get.nestedKey(1)
拿到这个 Navigator 的状态 (currentState
),然后对其进行 push/pop 操作。
- 为这个局部 Navigator 指定一个唯一标识
pages: [...]
- 这是 Navigator 2.0 风格的路由栈列表。
- 初始页面是
Tab1Home
。 - 所有 push/pop 都会在这个栈上操作。
onPopPage
- 当执行 pop 时会调用。
route.didPop(result)
表示通知 Navigator 这个页面已被弹出。- 如果不写,GetNavigator 默认会处理 pop,功能通常仍然正常。
Get.nestedKey(1)!.currentState!
Get.nestedKey(1)
- 这是 GetX 提供的方法,用来获取 指定 id 的局部 Navigator 的 GlobalKey。
- 参数
1
对应你在GetNavigator(key: Get.nestedKey(1))
里指定的 key。 - 它返回类型是
GlobalKey<NavigatorState>?
,可能为 null。
!
(非空断言)Get.nestedKey(1)!
表示你确信这个 key 不为空。- 如果 key 为 null,会抛出运行时异常。
.currentState
GlobalKey
有一个currentState
属性,返回它绑定的 Widget 的 State。- 对于
Navigator
或GetNavigator
来说,currentState
类型是NavigatorState
。 - 通过
NavigatorState
可以调用 push/pop 等导航方法。
最后一个
!
- 再次非空断言,保证
currentState
不为 null。 - 这意味着你确信对应的 Navigator 已经创建完成。
- 再次非空断言,保证
nav.push(MaterialPageRoute(...))
- 在
Tab1Page
的子导航器栈上 push 一个新页面DetailPage
。 - 类似于在普通 Navigator 上 push,但仅影响局部 Navigator。
- 在
nav.pop()
- 弹出
Tab1Page
子导航器栈顶部页面(即 DetailPage)。 - 栈回到初始页面
Tab1Home
。
- 弹出
原生 Navigator 2.0 与 GetX
// Navigator 2.0 风格 push 页面到 Tab1 的子导航器
final nav = Get.nestedKey(1)!.currentState!;
nav.push(
MaterialPageRoute(builder: (_) => const DetailPage()),
);
// 或者用 GetX 提供的方式:
// Get.to(() => const DetailPage(), id: 1);
final nav = Get.nestedKey(1)!.currentState!;
nav.pop();
// 或者用 GetX:
// Get.back(id: 1);
维度 | 原生 Navigator 2.0 | GetX |
---|---|---|
语法简洁 | ❌ 较繁琐,需要 GlobalKey + currentState | ✅ 简单 Get.to() / Get.back() |
嵌套路由支持 | ✅ 可用 GlobalKey 指定 navigator | ✅ 直接用 id 管理嵌套导航栈 |
高度自定义 | ✅ 完全可控 | ⚠️ 限制多一些 |
依赖 | ❌ 无额外依赖 | ✅ 依赖 GetX |
状态管理整合 | ⚠️ 需手动管理 | ✅ 可直接和 GetX 状态管理结合 |
- 小中型项目 + 想快速开发 + 使用 GetX 状态管理 → 直接用
Get.to(id: ...)
最方便。 - 大型项目 + 高度自定义路由/Transition/导航控制 → 原生 Navigator 2.0 更安全。
pages: [...]
GetNavigator(
key: Get.nestedKey(1),
pages: [
const MaterialPage(name: "/tab1/home", child: Tab1Home()),
],
onPopPage: (route, result) => route.didPop(result),
)
是什么
pages
是一个 Page 对象的列表,每个 Page 对象对应一个页面。它替代了传统 Navigator 1.0 的 onGenerateRoute
。
在 Navigator 2.0 风格里,页面不再由路由名动态生成,而是由这个 pages
列表直接控制 路由栈。
最常用的是 MaterialPage
或 CupertinoPage
:
MaterialPage(
name: "/tab1/home",
child: Tab1Home(),
)
name
:页面的唯一标识(类似路由名)。child
:页面 Widget。- 可以携带
arguments
等信息。
每个 Page 对象就是 Navigator 栈里的一个“节点”。
工作机制
构建 Navigator 栈
pages
列表中的顺序就是 Navigator 栈的顺序。栈的本质
- 栈(Stack)是 先进后出(LIFO)。
- 新元素总是 放在栈顶。
- 移除元素(pop)的时候,也是从 栈顶移除。
列表末尾的 Page 会显示在最上层。
pages: [ MaterialPage(name: "/tab1/home", child: Tab1Home()), // 底部 MaterialPage(name: "/tab1/detail", child: DetailPage()) // 栈顶 ]
pages[0] = 最底层页面(Home)
pages[last] = 栈顶页面(Detail) 👉 显示的就是这个
显示页面
- Navigator 会自动渲染
pages.last
对应的 Widget。
- Navigator 会自动渲染
Push 操作
新的页面会放在栈顶
final nav = Get.nestedKey(1)!.currentState!; nav.push(MaterialPageRoute(builder: (_) => const DetailPage())); Get.to(() => const DetailPage(), id: 1); ┌─────────────┐ │ NewPage │ ← 栈顶(刚 push 进去) ├─────────────┤ │ DetailPage │ ├─────────────┤ │ HomePage │ └─────────────┘
Pop 操作
当执行
Navigator.pop()
或Get.back(id: 1)
时:最上层 Page 会被移除。下一个 Page 自动显示。┌─────────────┐ │ DetailPage │ ← 变成栈顶 ├─────────────┤ │ HomePage │ └─────────────┘
为什么一开始只定义一个Page
因为示例只是初始状态,默认进入时只有一个 Tab1Home
。
pages: [
MaterialPage(name: "/tab1/detail", child: DetailPage()) // 底部
MaterialPage(name: "/tab1/home", child: Tab1Home()), // 栈顶
]
如果一开始就把 DetailPage
放进 pages,那用户一进入 Tab1 就会直接看到 DetailPage
(等于栈里已经有它了),不符合逻辑。
pages: [
MaterialPage(name: "/tab1/home", child: Tab1Home()), // 底部
MaterialPage(name: "/tab1/detail", child: DetailPage()) // 栈顶
]
如果把DetailPage
放在最底部,那么正常情况下,DetailPage
就永远的使用不到,除非一开始就pop
页面Tab1Home
无法使用toName方式
Get.toNamed("/tab1/home/detail", id: 1);: //这段代码报错
// 注意这句话: the Navigator must be provided with an onGenerateRoute handler
FlutterError (Navigator.onGenerateRoute was null, but the route named "/tab1/home/detail" was referenced. To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or pushNamedAndRemoveUntil), the Navigator must be provided with an onGenerateRoute handler. The Navigator was: NavigatorState#d8e84(tickers: tracking 1 ticker))
如果是 Navigator 2.0 风格 + GetNavigator,子导航器不支持 toNamed
,只能用 Get.to(() => Page(), id: ...)
。
只有 全局的 GetMaterialApp 才能用 toNamed()
,子导航器内部不支持命名路由,除非你强行加 onGenerateRoute
(但不推荐)。
onPopPage
onPopPage
是什么?
// 类型签名:
bool Function(Route route, dynamic result)? onPopPage
功能:当栈顶页面尝试出栈(pop)时,会触发这个回调。
返回值:
- true → 页面成功出栈
- false → 页面未出栈(阻止默认 pop 行为)
你使用 Navigator 2.0 / GetNavigator 来管理页面栈时,每个页面都是一个 Page
对象。
当用户点击返回按钮或者调用 Navigator.pop()
/ Get.back(id: ...)
时:
- Navigator 会把 pop 请求传给
onPopPage
- 你可以在这里做自定义处理(比如更新状态、做拦截)
- 决定是否允许页面真正出栈
route.didPop(result)
GetNavigator(
key: Get.nestedKey(1),
pages: [
const MaterialPage(name: "/tab1/home", child: Tab1Home()),
],
onPopPage: (route, result) => route.didPop(result),
)
route.didPop(result)
:route
代表当前栈顶的页面对应的Route
对象。didPop(result)
的作用:- 告诉这个
Route
自己可以被弹出(pop),即允许 Navigator 将它从栈中移除。 - 返回
true
表示页面出栈成功。 - 返回
false
表示页面不能被弹出(可以用来阻止返回,比如拦截表单未保存)。
- 告诉这个
onPopPage
回调本身 只是一个回调,用来做判断或逻辑处理:
- 你可以在这里打印日志、弹出确认框、统计等。
- 真正移除页面、改变导航栈状态的操作就是
route.didPop(result)
。
onPopPage
流程图
[用户点击返回按钮 / 调用 Get.back(id)]
|
v
[嵌套 Navigator 获取 ID 对应路由栈]
|
v
[调用 Navigator.pop() 或 route.didPop(result)]
|
v
┌───────────────┐
│ onPopPage回调 │
└───────────────┘
|
┌────────────┴─────────────┐
| |
route.didPop(result) = true route.didPop(result) = false
| |
v v
页面出栈成功,返回 result 页面不出栈
|
v
栈顶页面显示
|
v
生命周期方法 dispose / didPopNext 等被触发
用户触发 pop
- 可以是点击 AppBar 的返回按钮,或者调用
Get.back(id)
。
路由栈找到对应的 Navigator
Get.nestedKey(id)
会定位到对应子导航器的NavigatorState
。
执行 pop 行为
- 调用
Navigator.pop()
或route.didPop(result)
。
onPopPage 回调被执行
- 可以在这里做拦截、统计、日志等操作。
- 回调返回
true
→ 页面真正出栈 - 回调返回
false
→ 页面被阻止出栈
页面出栈后
- 栈顶页面显示
- 对应页面的生命周期方法(
dispose
、didPopNext
)被触发
GetNavigator
内部有默认 onPopPage
由于GetNavigator
内部有自定义的onPopPage
,所有不写也可以。除非我们需要做一些额外的处理。
- 自定义逻辑:比如你想在用户返回时弹出确认框或做一些状态清理。
- 拦截返回:可以返回
false
阻止出栈。 - 统计或日志:记录用户导航行为。
class GetNavigator extends Navigator {
GetNavigator({
GlobalKey<NavigatorState>? key,
bool Function(Route<dynamic>, dynamic)? onPopPage,
required List<Page> pages,
List<NavigatorObserver>? observers,
bool reportsRouteUpdateToEngine = false,
TransitionDelegate? transitionDelegate,
}) : super(
//keys should be optional
key: key,
onPopPage: onPopPage ??
(route, result) {
final didPop = route.didPop(result);
if (!didPop) {
return false;
}
return true;
},
reportsRouteUpdateToEngine: reportsRouteUpdateToEngine,
pages: pages,
observers: [
// GetObserver(),
if (observers != null) ...observers,
],
transitionDelegate:
transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
);
}
自定义逻辑再调用 didPop
onPopPage: (route, result) {
if (route.settings.name == "/tab1/detail") {
print("DetailPage pop");
// 自定义逻辑:比如你想在用户返回时弹出确认框或做一些状态清理。
// 拦截返回:可以返回 false 阻止出栈。
// 统计或日志:记录用户导航行为。
}
return route.didPop(result); // 保证正常出栈
}
为什么嵌套导航都不写在GetMaterialApp的getPages里面了
getPages
是全局路由表
GetMaterialApp
的 getPages
用来定义 全局可访问的命名路由:
void main() {
runApp(GetMaterialApp(
title: '全局路由示例',
initialRoute: '/tab1/home',
getPages: [
// Tab1 页面
GetPage(name: '/tab1/home', page: () => const Tab1Home()),
GetPage(name: '/tab1/detail', page: () => const DetailPage()),
// Tab2 页面
GetPage(name: '/tab2/home', page: () => const Tab2Home()),
],
));
}
初始状态
initialRoute: '/tab1/home'
- 应用启动时,GetX 会把
'/tab1/home'
页面推入 根导航器栈。 - 当前栈状态(根导航器):
Root Navigator Stack:
--------------------
| /tab1/home | <-- 栈顶
--------------------
注意:这里只有 一个栈,因为你把所有页面放到全局
getPages
里,没有使用嵌套 Navigator (Get.nestedKey
)。
如果跳转到详情页
Get.toNamed('/tab1/detail');
/tab1/detail
会被推入 同一个根导航器栈。- 栈状态:
Root Navigator Stack:
--------------------
| /tab1/home |
| /tab1/detail | <-- 栈顶
--------------------
pop()
会把/tab1/detail
出栈,回到/tab1/home
。
如果跳转到 Tab2 页面
Get.toNamed('/tab2/home');
/tab2/home
也在 同一个全局栈,不会像嵌套 Navigator 那样保留 Tab1 的状态。- 栈状态:
Root Navigator Stack:
--------------------
| /tab1/home |
| /tab1/detail |
| /tab2/home | <-- 栈顶
--------------------
⚠️ 注意:这里
/tab1/home
和/tab1/detail
仍然在栈里,如果你切换 Tab2 并不清空 Tab1 页面,会一直占用栈空间。
嵌套导航的场景
像你做的 Tab + IndexedStack:
IndexedStack(
children: [Tab1Page(), Tab2Page()],
)
- 每个 Tab 都有自己的 子导航器(Navigator / GetNavigator)。
- 每个 Navigator 有自己的 独立栈。
- 比如
Tab1
的页面栈和Tab2
的页面栈互不影响。
如果把嵌套导航的子页面放到 getPages
:
- 你需要使用 全局栈 来 push 页面。
- 就无法保证 “Tab1 内部跳转不影响 Tab2 栈”。
- Tab1 切到 Tab2,再切回 Tab1,本来应该保持 Tab1 栈状态,但如果用全局路由就容易被干扰。
所以嵌套导航一般这样做
- 局部路由栈 → 用
Navigator
/GetNavigator
+pages
或onGenerateRoute
。 - 全局路由栈 → 用
GetMaterialApp
+getPages
。 - 局部栈里的页面不需要写在全局
getPages
。
总结
类型 | 路由定义位置 | 栈的作用 | 示例操作 |
---|---|---|---|
全局栈 | GetMaterialApp.getPages | 全局 Navigator 栈 | Get.toNamed(‘/login’) |
嵌套子栈 | GetNavigator.pages / onGenerateRoute | Tab 内部独立栈,切 tab 不影响 | Get.to(…, id:1) |
💡 核心原因:嵌套导航需要 局部独立栈,如果放到全局 getPages
,就破坏了局部栈的隔离性和状态保持。
participatesInRootNavigator
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Navigator 1.0 + GetX 嵌套导航 Demo',
initialRoute: "/",
getPages: [
// 主页面
GetPage(name: "/", page: () => const MainPage()),
// Tab1 的路由(注意要指定 id:1)
GetPage(
name: "/tab1/home",
page: () => const Tab1Home(),
participatesInRootNavigator: false, // 不参与根路由
),
GetPage(
name: "/tab1/detail",
page: () => const DetailPage(),
participatesInRootNavigator: false,
),
// Tab2 的路由
GetPage(
name: "/tab2",
page: () => const Tab2Page(),
participatesInRootNavigator: false,
),
],
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("GetX+Navigator 1.0 嵌套导航 Demo")),
body: IndexedStack(
index: _index,
children: const [Tab1Page(), Tab2PageWrapper()],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Tab1"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Tab2"),
],
),
);
}
}
class Tab1Page extends StatelessWidget {
const Tab1Page({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(1),
initialRoute: "/tab1/home",
onGenerateRoute: (settings) {
return GetPageRoute(
settings: settings,
page: () {
if (settings.name == "/tab1/detail") return const DetailPage();
return const Tab1Home();
},
);
},
);
}
}
class Tab1Home extends StatelessWidget {
const Tab1Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab1 首页")),
body: Center(
child: ElevatedButton(
onPressed: () {
// 这里使用命名路由 + id
Get.toNamed("/tab1/detail", id: 1);
},
child: const Text("进入详情页"),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("详情页")),
body: Center(
child: ElevatedButton(
onPressed: () => Get.back(id: 1),
child: const Text("返回 Tab1"),
),
),
);
}
}
/// Tab2 我这里也用一个 Navigator 包装,方便以后拓展
class Tab2PageWrapper extends StatelessWidget {
const Tab2PageWrapper({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(2),
onGenerateRoute: (settings) {
return GetPageRoute(settings: settings, page: () => const Tab2Page());
},
);
}
}
class Tab2Page extends StatelessWidget {
const Tab2Page({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Tab2 页面")),
body: const Center(child: Text("这里是 Tab2")),
);
}
}
使用participatesInRootNavigator
实现 全局路由 + 嵌套 Navigator1.0
participatesInRootNavigator
的作用
取值 | 作用 |
---|---|
true |
页面会加入 根导航器 栈(Root Navigator),忽略子导航器栈) |
false |
页面 不参与根导航器栈,会根据 id 找到对应的 子导航器 来入栈 |
GetPage(
name: "/tab1/detail",
page: () => const DetailPage(),
participatesInRootNavigator: false,
)
不加入根栈。
当你用 Get.toNamed("/tab1/detail", id: 1)
时:
- GetX 找到
id=1
对应的嵌套 Navigator(Tab1Navigator)。 - 把
/tab1/detail
压入 Tab1Navigator 的栈顶。
如果不指定 id
,GetX 默认会加入根栈,这时就会忽略 Tab1 的嵌套栈。
participatesInRootNavigator: false
+ id: 1
= 页面只加入 指定的子导航器栈,不会污染全局根栈。
最终实现的效果图:
Root Navigator Stack
└── MainPage (根页面)
├── Tab1 Navigator (id=1)
│ ├── /tab1/home
│ └── /tab1/detail
└── Tab2 Navigator (id=2)
└── /tab2
缺点是,不能使用Navigator2.0
。因为从 GetX 4.6.x 之后,GetNavigator
已经和 Flutter 原生的 Navigator 2.0 完全对齐,不再支持 onGenerateRoute
。
也就是不能使用Get.toNamed
的方式生成页面。当然,还是可以使用Get.to
。
但是如果使用Get.to
全局配置的意义也就没了。