GetX:嵌套导航

什么是嵌套导航

普通导航

在 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")),
    );
  }
}
  1. Navigator

    • 相当于创建了一个局部导航器(nested navigator)。
    • 它有自己的路由栈,和 MainPage 外层的 Navigator 互不干扰
    • 所以 Tab1Page 下的跳转不会影响外层的底部导航。
  2. onGenerateRoute

    • 这是 Navigator 第一次构建时会调用的函数。
    • 它的任务就是告诉 Navigator“我初始要显示哪个页面?”
    • 在这里,它返回了一个 MaterialPageRoute,表示默认显示 Tab1Home
  3. MaterialPageRoute

    • 是 Flutter 提供的一个最常用的路由类,基于 Material Design 的页面切换。

    • 它内部帮你封装了:

      • 页面过渡动画(默认从右往左滑入,符合 Android 的习惯)。
      • 页面生命周期管理
    • 它需要一个 builder 参数,这里写的是:

      builder: (_) => const Tab1Home()
      

      表示:这个路由的内容就是 Tab1Home 页面

  4. Navigator.of(context).push

    • 这里的 Navigator.of(context) 会找到 最近的 Navigator,也就是 Tab1Page 里面的那个 嵌套 Navigator,而不是最外层的 MaterialApp Navigator。
    • 因此,DetailPage 只会在 Tab1 的导航栈里被 push,而不会影响主页面或者 Tab2。
    • 切换到 Tab2 再切换回来时,DetailPage 仍然存在,因为 IndexedStack 保留了状态。
  5. 返回时 Navigator.of(context).pop()

    • 同样是操作 子 Navigator 的栈,从 DetailPage 返回到 Tab1Home
    • 不会影响全局 Navigator,也不会跳回到 MainPage

路由栈示意图

有子 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);
  1. GetNavigator

    • 相当于一个 局部的 Navigator,它维护自己的路由栈。
    • 可以理解为 Navigator 的 GetX 版本。
  2. key: Get.nestedKey(1)

    • 为这个局部 Navigator 指定一个唯一标识 1
    • 这个 key 可以让我们在其他地方通过 Get.nestedKey(1) 拿到这个 Navigator 的状态 (currentState),然后对其进行 push/pop 操作。
  3. pages: [...]

    • 这是 Navigator 2.0 风格的路由栈列表。
    • 初始页面是 Tab1Home
    • 所有 push/pop 都会在这个栈上操作。
  4. onPopPage

    • 当执行 pop 时会调用。
    • route.didPop(result) 表示通知 Navigator 这个页面已被弹出。
    • 如果不写,GetNavigator 默认会处理 pop,功能通常仍然正常。
  5. 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。
      • 对于 NavigatorGetNavigator 来说,currentState 类型是 NavigatorState
      • 通过 NavigatorState 可以调用 push/pop 等导航方法。
    • 最后一个 !

      • 再次非空断言,保证 currentState 不为 null。
      • 这意味着你确信对应的 Navigator 已经创建完成。
  6. nav.push(MaterialPageRoute(...))

    • Tab1Page 的子导航器栈上 push 一个新页面 DetailPage
    • 类似于在普通 Navigator 上 push,但仅影响局部 Navigator。
  7. 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 列表直接控制 路由栈

最常用的是 MaterialPageCupertinoPage

MaterialPage(
  name: "/tab1/home",
  child: Tab1Home(),
)
  • name:页面的唯一标识(类似路由名)。
  • child:页面 Widget。
  • 可以携带 arguments 等信息。

每个 Page 对象就是 Navigator 栈里的一个“节点”。

工作机制

  1. 构建 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) 👉 显示的就是这个

  2. 显示页面

    • Navigator 会自动渲染 pages.last 对应的 Widget。
  3. Push 操作

    • 新的页面会放在栈顶

      final nav = Get.nestedKey(1)!.currentState!;
      nav.push(MaterialPageRoute(builder: (_) => const DetailPage()));
      
      Get.to(() => const DetailPage(), id: 1);
      
      ┌─────────────┐
      │ NewPage                      │  ← 栈顶(刚 push 进去)
      ├─────────────┤
      │ DetailPage                   │
      ├─────────────┤
      │ HomePage                     │
      └─────────────┘
      
  4. 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: ...) 时:

  1. Navigator 会把 pop 请求传给 onPopPage
  2. 你可以在这里做自定义处理(比如更新状态、做拦截)
  3. 决定是否允许页面真正出栈

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 → 页面被阻止出栈

页面出栈后

  • 栈顶页面显示
  • 对应页面的生命周期方法(disposedidPopNext)被触发

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 是全局路由表

GetMaterialAppgetPages 用来定义 全局可访问的命名路由

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 + pagesonGenerateRoute
  • 全局路由栈 → 用 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) 时:

  1. GetX 找到 id=1 对应的嵌套 Navigator(Tab1Navigator)。
  2. /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全局配置的意义也就没了。

×

喜欢就点赞,疼爱就打赏