Flutter 页面导航Navigator.push 与自适应导航模式(十四)

前言

上一篇文章中,我们用 Sliver 构建了可折叠导航栏、搜索框和按字母分组的联系人列表。但在小屏幕上,点击一个分组还没法跳转到联系人列表------因为我们还没实现导航。

今天这篇文章基于官方教程的「Stack Based Navigation」章节,也是整个 Flutter 入门系列的最后一课 。我们将学习 Flutter 中最基本的导航方式------基于栈的导航(Navigator.push / pop),并实现两种不同屏幕尺寸下的自适应导航模式。


一、什么是基于栈的导航?

"栈"(Stack)是一种后进先出的数据结构,就像一摞盘子------最后放上去的盘子最先被拿走。

Flutter 的导航系统也是这样工作的:

  • push:把新页面"压"到栈顶,覆盖当前页面
  • pop:把栈顶页面"弹出",回到下面的页面

二、实现小屏幕导航

更新 ContactGroupsPage,点击分组时跳转到联系人列表页:

scala 复制代码
class ContactGroupsPage extends StatelessWidget {
  const ContactGroupsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      // 当用户点击某个分组时触发的回调
      onListSelected: (list) => Navigator.of(context).push(
        // CupertinoPageRoute 创建 iOS 风格的页面跳转
        CupertinoPageRoute(
          // title:新页面导航栏中显示的标题
          title: list.title,
          // builder:构建新页面的内容
          builder: (context) => ContactListsPage(listId: list.id),
        ),
      ),
    );
  }
}

这段代码包含了导航的三个核心要素:

Navigator.of(context) :从 Widget 树中获取最近的 Navigator。Flutter 的每个应用都有一个内置的 Navigator,你不需要手动创建。

.push() :把新路由压入导航栈,新页面从右侧滑入覆盖当前页面。

CupertinoPageRoute:iOS 风格的路由,自带以下特性:

  • 从右向左的滑入动画
  • 自动生成返回按钮
  • 支持右滑手势返回
  • 正确处理标题显示

2.2 返回上一页

返回上一页(pop)不需要你写代码------CupertinoPageRoute 自动在导航栏添加了返回按钮,用户也可以从左边缘右滑返回。如果你需要在代码中主动返回,可以调用:

scss 复制代码
// 手动返回上一页
Navigator.of(context).pop();

三、实现大屏幕侧边栏

大屏幕不需要页面跳转------点击左侧分组直接更新右侧内容。我们创建两个专用组件。

3.1 侧边栏组件

在 contact_groups.dart 底部添加:

scala 复制代码
// ContactGroupsSidebar:大屏幕专用的侧边栏组件
// 与 ContactGroupsPage 复用同一个 _ContactGroupsView
// 区别:点击分组不跳转页面,而是调用回调更新右侧面板
class ContactGroupsSidebar extends StatelessWidget {
  const ContactGroupsSidebar({
    super.key,
    required this.selectedListId,   // 当前选中的分组 ID(用于高亮)
    required this.onListSelected,   // 点击分组时的回调
  });

  final int selectedListId;
  final Function(int) onListSelected;

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: selectedListId,
      // 点击时不导航,而是把分组 ID 传给父组件
      // 父组件(AdaptiveLayout)会用 setState 更新右侧面板
      onListSelected: (list) => onListSelected(list.id),
    );
  }
}

3.2 详情面板组件

在 contacts.dart 底部添加:

scala 复制代码
// ContactListDetail:大屏幕专用的详情面板组件
// 复用 _ContactListView,但隐藏返回按钮
// 因为在大屏模式下,导航由侧边栏控制,不需要返回按钮
class ContactListDetail extends StatelessWidget {
  const ContactListDetail({super.key, required this.listId});

  final int listId;

  @override
  Widget build(BuildContext context) {
    return _ContactListView(
      listId: listId,
      // false = 不自动显示返回按钮
      // 大屏模式下用户通过点击侧边栏切换内容,不需要"返回"
      automaticallyImplyLeading: false,
    );
  }
}

四、连接到自适应布局

更新 adaptive_layout.dart,将占位文字替换为真正的侧边栏和详情面板:

scala 复制代码
import 'package:flutter/cupertino.dart';
import 'package:rolodex/screens/contact_groups.dart';
import 'package:rolodex/screens/contacts.dart';

const largeScreenMinWidth = 600;

class AdaptiveLayout extends StatefulWidget {
  const AdaptiveLayout({super.key});

  @override
  State<AdaptiveLayout> createState() => _AdaptiveLayoutState();
}

class _AdaptiveLayoutState extends State<AdaptiveLayout> {
  // 当前选中的分组 ID
  int selectedListId = 0;

  // 侧边栏点击分组时调用
  void _onContactListSelected(int listId) {
    setState(() {
      selectedListId = listId;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isLargeScreen = constraints.maxWidth > largeScreenMinWidth;

        if (isLargeScreen) {
          return _buildLargeScreenLayout();
        } else {
          // 小屏幕:显示分组列表
          // 点击分组 → Navigator.push → 联系人列表页
          return const ContactGroupsPage();
        }
      },
    );
  }

  // 大屏幕布局:侧边栏 + 详情面板
  Widget _buildLargeScreenLayout() {
    return CupertinoPageScaffold(
      backgroundColor: CupertinoColors.extraLightBackgroundGray,
      child: SafeArea(
        child: Row(
          children: [
            // 左侧:侧边栏(固定 320px)
            SizedBox(
              width: 320,
              // 使用侧边栏组件(点击分组不导航,而是更新 selectedListId)
              child: ContactGroupsSidebar(
                selectedListId: selectedListId,
                onListSelected: _onContactListSelected,
              ),
            ),
            // 分隔线
            Container(width: 1, color: CupertinoColors.separator),
            // 右侧:详情面板(占满剩余空间)
            Expanded(
              // 根据 selectedListId 显示对应分组的联系人
              child: ContactListDetail(listId: selectedListId),
            ),
          ],
        ),
      ),
    );
  }
}

五、两种导航模式对比

特性 小屏幕(手机) 大屏幕(平板/电脑)
导航方式 Navigator.push(栈导航) setState 更新右侧面板
页面切换 新页面从右滑入覆盖 右侧面板内容直接替换
返回方式 返回按钮 / 右滑手势 点击侧边栏其他分组
导航栈 有(push/pop) 无(不涉及导航栈)
路由组件 CupertinoPageRoute 不使用
代码复用 _ContactGroupsView _ContactGroupsView(同一个!)

核心思想:底层视图组件是复用的 (_ContactGroupsView 和 _ContactListView),只是外层包装不同------小屏用 ContactGroupsPage(导航式),大屏用 ContactGroupsSidebar(回调式)。


六、整个系列回顾

恭喜你!16 课全部完成,让我们回顾整个学习路径:

阶段 课程 项目 核心收获
Flutter UI 101 1-8 Birdle 猜词游戏 Widget 基础、布局、StatefulWidget、动画
状态管理 9-12 维基百科阅读器 HTTP 请求、MVVM、ChangeNotifier、ListenableBuilder
Flutter UI 102 13-16 Rolodex 通讯录 Cupertino 风格、自适应布局、Sliver 滚动、导航

从零开始,你已经构建了三个完整的 Flutter 应用,掌握了从基础到进阶的核心技能。接下来可以尝试构建自己的应用,或者继续深入学习 Flutter 的动画、测试、发布等主题。

加油,Flutter 开发者!

参考资料:Flutter 官方教程 - Stack Based Navigation

相关推荐
卸任8 小时前
Vibe coding工具使用心得
前端·ai编程
brien98 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance9 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M9 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写9 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李9 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易9 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马9 小时前
前沿视界
前端
kyriewen9 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易10 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员