Flutter学习笔记——第三章:页面跳转与路由管理(Navigator1.0)

74 阅读7分钟

前言:

在 Flutter 里,页面跳转其实不是“跳”,而是“压栈 / 出栈” 。如果你只会 Navigator.push,那你只会了 30%
需要注意的是:本文基于 Flutter Navigator 1.0 体系, 不涉及 Router / Navigator 2.0。

一、Flutter 的页面跳转本质是什么?

在Android中: Activity : Activity → 启动 / finish
在 iOS 里: ViewController → push / pop

在Flutter中

  • 页面 Widget 是被包装在 Route 中,再由 Navigator 管理
  • 页面栈 = Navigator 管理的 Stack
    示意图:

生成 Flutter 页面跳转图.png

  • push → 压一个页面到栈顶(跳转到C,就是将C压入栈顶)
  • pop → 把栈顶页面弹掉(返回到B,就是将C从栈顶弹出)
  • 永远只显示 栈顶页面

Navigator = 页面栈管理器

二、最基础的页面跳转(必须会)

1、push:跳转到新界面

Navigator.push(
 context,
 MaterialPageRoute(
   builder: (context) => const DetailPage(),
 ),
);

作用:将DetailPage压入页面栈顶,显示在最上方。

根据示意图生成图片.png

2、pop

Navigator.pop(context);

作用:把当前页面从页面栈移除。

根据示意图生成图片 (1).jpg

三、页面跳转时传值

1、跳转时传值(push → 传参数)

场景:列表 → 详情页

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailPage(
      title: 'Flutter 真香',
      id: 1001,
    ),
  ),
);

DetailPage 接收参数

class DetailPage extends StatelessWidget {
  final String title;
  final int id;

  const DetailPage({
    super.key,
    required this.title,
    required this.id,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Text('文章ID:$id'),
    );
  }
}

2、返回时带结果(pop → 回传数据)

场景:选择页 → 返回选中的结果

final result = await Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const SelectPage(),
  ),
);

// result 就是 pop 传回来的值
print('选择结果:$result');

SelectPage 中返回数据

Navigator.pop(context, '南京');

dd0c27d4-d08c-4833-a5db-c4cab0c79d4b.png Navigator 的 push 方法会返回一个 Future, 当页面通过 pop(result) 退出时,该 Future 会完成并携带 result。

四、命令路由(Named Routes)

当页面多了,你一定会遇到这个问题: MaterialPageRoute 写得太多了,看着好乱 。那如何解决这个问题呢?这一章节将给出解决方案:

第一步:定义路由表

MaterialApp(
  routes: {
    '/': (context) => const HomePage(),//主页
    '/detail': (context) => const DetailPage(),//详情页
  },
);

第二步:使用命名路由跳转

Navigator.pushNamed(context, '/detail');

第三步:返回

Navigator.pop(context);

注意:命名路由的缺点(必须知道),小项目可以用,大项目慎用

  • 参数不直观
  • 复杂页面传参容易乱
  • 不利于重构
  • 编译期无法校验路由是否存在(字符串路由)

五、带参数的命名路由

命名路由同样存在携带参数的需求,请参照本章节

1、使用 arguments

Navigator.pushNamed(
  context,
  '/detail',
  arguments: {
    'id': 1001,
    'title': 'Flutter 路由',
  },
);

2、在目标页面获取

final args = ModalRoute.of(context)!.settings.arguments as Map;
final id = args['id'];
final title = args['title'];

看上去非常美好,但其实存在一些问题,在大项目中偶尔会让开发者崩溃。
部分问题如下:

  • 没类型检查
  • key 写错不会报错
    这就是为什么很多人后面会转 go_router / auto_route

六、pushReplacement / pushAndRemoveUntil(非常重要)

大家是不是经常遇到这样的场景,登录成功跳转到首页后,我们需要移除登录页。那在Flutter中如何解决这个问题?

pushReplacement —— 替换当前页面

场景:登录页 → 首页(不能返回登录页)

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => const HomePage()),
);

注意:替换当前页面(只移除最顶层一个),登录成功后必用


pushAndRemoveUntil —— 清空历史栈

场景:账号被踢下线 → 跳转到登录页面,并移除所有页面

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (_) => const LoginPage()),
  (route) => false,
);

常用于:

  • 注册/登录成功后跳转到主界面(或反过来登出后回登录)
  • 强制重置导航栈(比如深层页面跳到登录,避免返回后还在已登录状态)
    也许你注意到了这段代码(route) => false,如果改为(route) => true会发生什么? 这个参数啥都不移除”(几乎没人这么写,没意义)

下面的表格将列出几种常见的用法

代码写法效果典型使用场景用户按返回键会发生什么
Navigator.push(...)正常叠加新页面普通页面跳转返回上一页
Navigator.pushReplacement(...)替换当前页面(只移除最顶层一个)从登录页跳转到首页,希望用户无法返回登录页,但仍保留 splash 等更早的页面(如果存在)可能返回 splash 或其他
Navigator.pushAndRemoveUntil(..., (route) => false)跳转 + 清空整个栈登录成功后跳首页、注册完成、登出后回登录页通常会直接退出应用(无历史可回)
Navigator.pushAndRemoveUntil(..., ModalRoute.withName('/home'))跳转 + 移除直到名为 '/home' 的路由为止跳到已存在的首页,清除中间页面返回到 /home 之前的页面

最后一种可能比较特殊,我使用AI生成示意图,折腾了半天也不满意,算了,直接用文本表示吧。
执行前:

栈底(最早的页面)                                 
┌───────────────────────┐
│ /splash  (根,通常不移除) │
├───────────────────────┤
│ /login                │
├───────────────────────┤
│ /home                 │  ← 这里 name == '/home',关键停止点
├───────────────────────┤
│ /settings             │
├───────────────────────┤
│ /profile              │  ← 当前页面
└───────────────────────┘
栈顶(当前页面)

执行如下代码:

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (_) => const NewPage()),
  ModalRoute.withName('/home'),
);

执行后栈变化

  • 从栈顶(/profile)开始逐个 pop页面
  • pop /profile → pop /settings
  • 遇到 /home 时,predicate 返回 true → 停止 pop,保留 /home 及下面所有页面
  • 然后 push NewPage 到栈顶

执行结果:

栈底(最早的页面)                                 
┌───────────────────────┐
│ /splash               │
├───────────────────────┤
│ /login                │
├───────────────────────┤
│ /home                 │  ← 保留,从这里开始不 pop
├───────────────────────┤
│ NewPage               │  ← 新页面(当前可见)
└───────────────────────┘
栈顶(当前页面)

速记口诀

  • (route) => false = “全清空”
  • (route) => true = “啥都不移除”
  • ModalRoute.withName('xxx') = “移除到名为 xxx 的页面为止,xxx 保留”

七、Navigator.of(context) 到底是什么?

工作中,你大概率会碰见如下两种写法:

Navigator.push(context, route);
Navigator.of(context).push(route);

这两种写法本质是一致的,是一个 InheritedWidget of(context) 是从 Widget 树里找到最近的 Navigator 常见的坑:

//返回上一次页面,弹窗确认
showDialog(
  context: context,
  builder: (context) {
    return AlertDialog(
      actions: [
        TextButton(
          onPressed: () {
            Navigator.pop(context); // 只会关闭 Dialog
          },
          child: Text('确定'),
        ),
      ],
    );
  },
);

你会发现,点击“确认”只是关闭了弹窗,并没有预想的返回上一个页面。
原因: 这里的 Navigator.pop(context) 中的 context 是 Dialog的context ,而不是页面的context。因此,它只会关闭当前的Dialog,而不会返回上一页。

关键知识点

  1. Context作用域 :
    • showDialog 中的 context 参数是调用页面的context
    • builder 函数中的 context 参数是Dialog的context
    • 两者是不同的对象,作用域不同
  2. Navigator.pop() :
    • 总是作用于当前context所在的导航栈
    • 对于Dialog来说,showDialog 会通过 Navigator 再 push 一个 DialogRoute,因此 pop(context) 只会弹出该 Route,而不会影响页面 Route。
    • 对于页面来说,它在主导航栈中
  3. 导航栈原理 :
    • Flutter使用导航栈来管理页面和弹窗
    • 每个页面和弹窗都是栈中的一个元素
    • pop() 操作会移除栈顶的元素

修复:

// 在页面中使用
 showDialog(
      context: context,
      builder: (dialogContext) {
        return AlertDialog(
          title: Text('确认操作'),
          content: Text('确定要返回上一页吗?'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.pop(dialogContext); // 关闭Dialog
              },
              child: Text('取消'),
            ),
            TextButton(
              onPressed: () {
                Navigator.pop(dialogContext); // 先关闭Dialog
                Navigator.pop(context); // 再返回上一页
              },
              child: Text('确定'),
            ),
          ],
        );
      },
    )

📌 记住:想关闭A,就用A创建时所在层级的 context。

八、实际项目的推荐写法

封装一个跳转工具类:

class AppNavigator {
  static push(BuildContext context, Widget page) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => page),
    );
  }

  static pop(BuildContext context, [result]) {
    Navigator.pop(context, result);
  }
}

使用时:

AppNavigator.push(context, const DetailPage());

需要注意:⚠️ 简单项目可以这样封装,中大型项目建议封装 Route 或使用路由框架。

九、什么时候该换路由框架?

场景Navigatorgo_router
Demo / 学习
中小项目可选
多 Tab / Web / 深链
强类型路由
对于我这类新手,我感觉还是先从简单的来,先把Navigator用熟,熟悉后可慢慢切换至go_router

十、记忆点总结(学习期间总结,帮助快速记忆)

  • (页面跳转)push / pop = 压栈 / 出栈
  • 页面传值 = 构造函数
  • 页面回传 = Future + pop(result)
  • 登录后跳首页 = pushReplacement
  • 清空历史 = pushAndRemoveUntil
  • Navigator 是 Widget 树里的“栈管理器”

写在最后

十一、2026 年视角:Navigator 1.0 还香吗?

写这篇文章时,我特意标注了基于 Navigator 1.0,因为:

  • 它简单、直观,上手零成本
  • 所有代码在最新 Flutter(3.24+ / 4.x)里都能跑
  • 小项目、学习、纯移动端依然是最高效的选择

但如果你计划做:

  • 支持 Web(URL 同步、前进后退)
  • 深层链接(从推送/分享打开特定页面)
  • 复杂嵌套导航(TabBar + Drawer + 子页面栈)
  • 类型安全路由(避免字符串拼错)

那建议尽快迁移到 go_router(Flutter 官方推荐路由包)。

go_router 本质是 Navigator 2.0 的“人性化封装”,语法类似命名路由,但支持:

  • /users/:id 路径参数
  • context.go('/home') / context.push('/detail')
  • redirect(登录校验)
  • ShellRoute(带底部导航的壳)
  • 强类型(可选 + 代码生成)

后续我会继续写 go_router 系列,敬请期待!

一句话:先把 Navigator 1.0 用熟,再优雅升级到 go_router —— 这才是大多数 Flutter 开发者的学习路径。