跳转至正文

基于栈的导航

学习如何在 Flutter 应用中从一个页面导航到另一个页面。

学习使用 Navigator.push 在屏幕之间导航,并为不同屏幕尺寸实现自适应导航模式。

你将完成的内容

使用 Navigator.push 在屏幕之间导航
使用 CupertinoPageRoute 实现 iOS 风格转场
为每种屏幕尺寸创建不同的导航模式

步骤

1

简介

既然你已理解 sliver 与滚动,就可以实现屏幕之间的导航。在本课中,你将更新小屏视图,使得点击联系人分组时,会导航到该分组的联系人列表。

首先,还原自适应布局 widget 中的更改,使其在小屏上默认显示 ContactGroupsPage

dart
class _AdaptiveLayoutState extends State<AdaptiveLayout> {
  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 {
          return const ContactGroupsPage(); // Reverted
        }
      },
    );
  }
  // ···
}
2

为联系人分组添加导航

ContactGroupsPage 已使用 _ContactGroupsView 并为其提供了回调。需要更新该回调,使得点击分组时进行导航,而不是将分组打印到控制台。

确保 lib/screens/contact_groups.dart 中的 onListSelected 回调和 import 实现如下:

lib/screens/contact_groups.dart
dart
import 'contacts.dart';

class ContactGroupsPage extends StatelessWidget {
  const ContactGroupsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      onListSelected: (list) => Navigator.of(context).push(
        CupertinoPageRoute<void>(
          title: list.title,
          builder: (context) => ContactListsPage(listId: list.id),
        ),
      ),
    );
  }
}

这一小段代码包含本页最重要的新信息。

Navigator.of(context) 从 widget 树中获取最近的 Navigator widget。 push 方法向 Navigator 的栈添加新路由,并显示 builder 属性返回的 widget。

这是使用基于栈的导航的最基本实现,新屏幕被 push 到当前屏幕之上。要返回上一屏幕,可使用 Navigator.pop 方法。

CupertinoPageRoute 创建 iOS 风格的页面转场,具有以下特性:

  • 从右侧滑入的动画。

  • 自动支持返回按钮。

  • 正确的标题处理。

  • 支持滑动返回手势。

3

为大屏创建侧边栏组件

对于大屏,你需要一个不进行导航、而是更新主内容区域的侧边栏。得益于上一步的重构,创建该组件更加直接。将此 widget 添加到 lib/screens/contact_groups.dart 底部:

dart
/// A sidebar component for selecting contact groups on large screens.
class ContactGroupsSidebar extends StatelessWidget {
  const ContactGroupsSidebar({
    super.key,
    required this.selectedListId,
    required this.onListSelected,
  });

  final int selectedListId;
  final void Function(int) onListSelected;

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: selectedListId,
      onListSelected: (list) => onListSelected(list.id),
    );
  }
}

该侧边栏组件复用 _ContactGroupsView 并提供不同的回调。它不进行导航,而是用被点击列表的 ID 调用 onListSelected。它还将 selectedListId 传给 _ContactGroupsView,以便高亮显示选中项。

4

为大屏创建详情视图

对于大屏布局,你需要一个不显示导航控件的详情视图。与侧边栏一样,可以通过复用 _ContactListView 来重新创建。将此 widget 添加到你的 contacts.dart 文件底部:

dart
class ContactListDetail extends StatelessWidget {
  const ContactListDetail({super.key, required this.listId});

  final int listId;

  @override
  Widget build(BuildContext context) {
    return _ContactListView(listId: listId, automaticallyImplyLeading: false);
  }
}

详情视图复用 _ContactListView,并将 automaticallyImplyLeading 参数设为 false 以隐藏返回按钮,因为导航由侧边栏处理。

5

将侧边栏连接到自适应布局

现在,将侧边栏连接到你的自适应布局。更新 adaptive_layout.dart 文件以导入必要文件并更新大屏布局:

dart
import 'package:flutter/cupertino.dart';

import 'contact_groups.dart';
import 'contacts.dart';

然后更新 _buildLargeScreenLayout 方法:

dart
Widget _buildLargeScreenLayout() {
  return CupertinoPageScaffold(
    backgroundColor: CupertinoColors.extraLightBackgroundGray,
    child: SafeArea(
      child: Row(
        children: [
          SizedBox(
            width: 320,
            child: ContactGroupsSidebar(
              selectedListId: selectedListId,
              onListSelected: _onContactListSelected,
            ),
          ),
          Container(width: 1, color: CupertinoColors.separator),
          Expanded(child: ContactListDetail(listId: selectedListId)),
        ],
      ),
    ),
  );
}

这段代码创建经典的菜单-详情布局,侧边栏控制详情区域的内容。

6

测试自适应导航行为

热重载应用并测试导航:

小屏(宽度 < 600px):

  • 点击联系人分组以导航到联系人详情。

  • 使用返回按钮或滑动手势返回。

  • 这是经典的基于栈的导航流程。

大屏(宽度 > 600px):

  • 在侧边栏中点击联系人分组以更新详情视图。

  • 没有导航栈。选择会更新内容区域。

  • 这是主从 (master-detail) 界面模式。

应用会根据屏幕尺寸自动选择合适的导航模式。这在手机和平板上都能提供最佳体验。

7

回顾

你已完成的内容

以下是你本课构建与学习内容的摘要。
使用 Navigator.push 在屏幕之间导航

Navigator.of(context).push 向导航栈添加新路由。这是基于栈的导航的基础,屏幕彼此叠加上去,通过 pop 返回。

使用 CupertinoPageRoute 实现 iOS 风格转场

CupertinoPageRoute 提供原生 iOS 导航特性支持:从右侧滑入的动画、自动返回按钮、正确的标题处理,以及滑动返回手势支持。

实现了自适应导航模式

你为小屏和大屏设置了不同的导航模式。小屏使用基于栈的导航,点击分组会 push 新屏幕。大屏使用主从模式,选择分组会更新详情面板而无需导航。

完成了 Rolodex 应用

你已构建完整的 iOS 风格联系人应用,具备自适应布局、高级滚动、带搜索的可折叠标题和响应式导航。这些都是生产应用中常用的模式!

8

自测

导航测验

1 / 2
Navigator.of(context).push 的作用是什么?
  1. 用新屏幕替换当前屏幕。

    不正确。

    push 会添加到栈;pushReplacement 才会替换当前屏幕。

  2. 向导航栈添加新路由,显示在当前屏幕之上。

    正确!

    push 将新路由添加到栈,使用户可以返回上一屏幕。

  3. 从导航栈移除当前屏幕。

    不正确。

    那是 pop 的作用;push 会添加新屏幕。

  4. 在当前屏幕上方打开对话框。

    不正确。

    对话框使用 showDialog;Navigator.push 用于导航到全屏界面。

Navigator.of(context).pop() 的作用是什么?
  1. 关闭整个应用。

    不正确。

    pop 仅移除当前路由;不会关闭应用。

  2. 从导航栈移除当前路由,返回上一屏幕。

    正确!

    pop 从栈顶移除路由,露出其下的屏幕。

  3. 清除所有路由并显示主屏幕。

    不正确。

    那需要 popUntil 或 pushAndRemoveUntil;pop 仅移除栈顶路由。

  4. 用新数据刷新当前屏幕。

    不正确。

    pop 用于返回导航;要刷新需使用 setState 或其他状态管理。