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

相关推荐
小凡同志2 小时前
那个复制粘贴了二十次 loading 的下午
前端·vue.js
HelloReader2 小时前
Flutter 底层原理揭秘框架如何工作(十五)
前端
南篱2 小时前
前端必看:一口气搞懂跨域是什么、为什么、怎么解决
前端·javascript·面试
qq_406176142 小时前
Vue 插槽与组件传参:从入门到精通
前端·javascript·vue.js
三年三月2 小时前
Redux 技术栈使用总结
前端·react.js
Tody Guo2 小时前
OpenClaw与企业微信的定时任务设定
前端·github·企业微信
张雨zy2 小时前
Vue 的 v-if 与 v-show,Android 的 GONE 与 INVISIBLE
android·前端·vue.js
sudo_明天上线2 小时前
React Compiler 技术原理解析
前端
xjf77112 小时前
Vue转TypeDOM的AI训练方案
前端·vue.js·人工智能·typedom