前言
上一篇文章中,我们用 LayoutBuilder 实现了自适应布局——大屏幕并排显示、小屏幕单页显示。但侧边栏和详情面板里都是占位文字,还没有真正的联系人列表。
今天这篇文章基于官方教程的「Scrolling and Slivers」章节,我们将学习 Flutter 中最强大的滚动机制——Sliver。通过它,你可以实现 iOS 通讯录那样的效果:滑动时导航栏自动折叠、搜索框隐藏在顶部、联系人按字母分组排列。
一、什么是 Sliver?
1.1 Sliver vs 普通 Widget
在 Flutter 中,普通 Widget(如 Column、Row、Container)可以在任何地方使用。而 Sliver 是一种专门为滚动设计的特殊 Widget,只能放在滚动视图(如 CustomScrollView)内部。
你可以把它们想象成两种不同的"积木":普通 Widget 是通用积木,哪里都能摆;Sliver 是专用的"滚轮积木",只能放在滑轨上。
1.2 核心规则
// ✅ 正确:Sliver 放在 CustomScrollView 的 slivers 列表中
CustomScrollView(
slivers: [
CupertinoSliverNavigationBar(...), // ← Sliver 组件
SliverList(...), // ← Sliver 组件
],
)
// ❌ 错误:普通 Widget 不能直接放在 slivers 中
CustomScrollView(
slivers: [
Text('Hello'), // ← 报错!Text 不是 Sliver
],
)
// ✅ 解决:用 SliverToBoxAdapter 包裹普通 Widget
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Text('Hello')), // ← 正确!
],
)
1.3 常用 Sliver 组件
| Sliver 组件 | 作用 |
|---|---|
CupertinoSliverNavigationBar | iOS 风格的可折叠导航栏 |
SliverList | 滚动列表(类似 ListView 的 Sliver 版本) |
SliverGrid | 滚动网格 |
SliverFillRemaining | 填满剩余空间(用于放普通 Widget) |
SliverToBoxAdapter | 将普通 Widget 转为 Sliver |
二、构建联系人分组列表
2.1 创建可复用的 _ContactGroupsView
为了让分组列表在小屏幕(作为主页面)和大屏幕(作为侧边栏)中都能复用,我们创建一个私有组件 _ContactGroupsView。
更新 lib/screens/contact_groups.dart:
import 'package:flutter/cupertino.dart';
import 'package:rolodex/data/contact.dart';
import 'package:rolodex/data/contact_group.dart';
import 'package:rolodex/main.dart';
// ContactGroupsPage:公开的页面组件
// 小屏幕模式下直接显示此页面
class ContactGroupsPage extends StatelessWidget {
const ContactGroupsPage({super.key});
@override
Widget build(BuildContext context) {
return _ContactGroupsView(
selectedListId: 0,
onListSelected: (list) {
// 下一课(导航)会实现页面跳转
debugPrint(list.toString());
},
);
}
}
// _ContactGroupsView:私有的可复用视图
// 包含 CustomScrollView + Sliver 实现可折叠导航栏 + 分组列表
class _ContactGroupsView extends StatelessWidget {
const _ContactGroupsView({
required this.onListSelected,
this.selectedListId,
});
final int? selectedListId;
// 回调函数:用户点击某个分组时调用
final Function(ContactGroup) onListSelected;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
// ===== CustomScrollView =====
// Sliver 组件的容器,所有 Sliver 都放在 slivers 列表中
child: CustomScrollView(
slivers: [
// ===== 可折叠导航栏 =====
// 向下滚动时,大标题会折叠成小标题,节省屏幕空间
const CupertinoSliverNavigationBar(
largeTitle: Text('Lists'),
),
// ===== 分组列表 =====
// SliverFillRemaining 填满导航栏下方的所有剩余空间
// 内部放普通 Widget(非 Sliver)
SliverFillRemaining(
// ValueListenableBuilder 监听分组数据变化
// 当数据更新时自动重绘列表
child: ValueListenableBuilder<List<ContactGroup>>(
valueListenable: contactGroupsModel.listsNotifier,
builder: (context, contactLists, child) {
// 定义图标:全部联系人用群组图标,其他用双人图标
const groupIcon = Icon(
CupertinoIcons.group, weight: 900, size: 32,
);
const pairIcon = Icon(
CupertinoIcons.person_2, weight: 900, size: 24,
);
// CupertinoListSection.insetGrouped:iOS 风格的圆角分组列表
return CupertinoListSection.insetGrouped(
header: const Text('iPhone'),
children: [
for (final ContactGroup contactList in contactLists)
CupertinoListTile(
// 根据分组类型显示不同图标
leading: contactList.id == 0 ? groupIcon : pairIcon,
title: Text(contactList.label),
// 右侧显示联系人数量和箭头
trailing: _buildTrailing(
contactList.contacts, context,
),
// 点击时调用回调
onTap: () => onListSelected(contactList),
),
],
);
},
),
),
],
),
);
}
// 构建列表项右侧的"数量 + 箭头"
Widget _buildTrailing(List<Contact> contacts, BuildContext context) {
final TextStyle style = CupertinoTheme.of(context)
.textTheme.textStyle
.copyWith(color: CupertinoColors.systemGrey);
return Row(
mainAxisSize: MainAxisSize.min, // 让 Row 只占内容需要的宽度
children: [
Text(contacts.length.toString(), style: style),
const Icon(
CupertinoIcons.forward,
color: CupertinoColors.systemGrey3,
size: 18,
),
],
);
}
}
三、构建联系人列表(带搜索和字母索引)
更新 lib/screens/contacts.dart:
import 'package:flutter/cupertino.dart';
import 'package:rolodex/data/contact.dart';
import 'package:rolodex/data/contact_group.dart';
import 'package:rolodex/main.dart';
// ContactListsPage:公开的页面组件
class ContactListsPage extends StatelessWidget {
const ContactListsPage({super.key, required this.listId});
final int listId;
@override
Widget build(BuildContext context) {
return _ContactListView(listId: listId);
}
}
// _ContactListView:私有的可复用联系人列表视图
// 包含:可折叠导航栏 + 搜索框 + 按字母分组的联系人列表
class _ContactListView extends StatelessWidget {
const _ContactListView({
required this.listId,
this.automaticallyImplyLeading = true,
});
final int listId;
// 是否自动显示返回按钮(大屏幕侧边栏模式不需要)
final bool automaticallyImplyLeading;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: ValueListenableBuilder<List<ContactGroup>>(
valueListenable: contactGroupsModel.listsNotifier,
builder: (context, contactGroups, child) {
// 根据 listId 获取对应的分组数据
final ContactGroup contactList =
contactGroupsModel.findContactList(listId);
// 获取按字母分组的联系人 Map
// 如 {'A': [Alex, Anna], 'B': [Ben], ...}
final AlphabetizedContactMap contacts =
contactList.alphabetizedContacts;
return CustomScrollView(
slivers: [
// ===== 可折叠导航栏 + 搜索框 =====
// .search 构造函数提供集成搜索功能
// 向下滚动时,大标题折叠,搜索框隐入导航栏
CupertinoSliverNavigationBar.search(
largeTitle: Text(contactList.title),
automaticallyImplyLeading: automaticallyImplyLeading,
// iOS 风格的搜索输入框
searchField: const CupertinoSearchTextField(
// 右侧麦克风图标
suffixIcon: Icon(CupertinoIcons.mic_fill),
suffixMode: OverlayVisibilityMode.always,
),
),
// ===== 按字母分组的联系人列表 =====
// SliverList.list 接收普通 Widget 列表
// 将它们变成可滚动的 Sliver 内容
SliverList.list(
children: [
const SizedBox(height: 20),
// 遍历每个字母分组,生成一个 ContactListSection
...contacts.keys.map(
(String initial) => ContactListSection(
lastInitial: initial,
contacts: contacts[initial]!,
),
),
],
),
],
);
},
),
);
}
}
// ContactListSection:单个字母分组
// 如 "A" 分组下显示 Alex Anderson、Anna Haro 等
class ContactListSection extends StatelessWidget {
const ContactListSection({
super.key,
required this.lastInitial, // 字母(如 'A')
required this.contacts, // 该字母下的联系人列表
});
final String lastInitial;
final List<Contact> contacts;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsDirectional.fromSTEB(20, 0, 20, 0),
child: Column(
children: [
const SizedBox(height: 15),
// 字母标题(如 "A"、"B")
Align(
alignment: AlignmentDirectional.bottomStart,
child: Text(
lastInitial,
style: const TextStyle(
color: CupertinoColors.systemGrey,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
// 该字母下的联系人列表
CupertinoListSection(
backgroundColor: CupertinoColors.systemBackground,
dividerMargin: 0,
additionalDividerMargin: 0,
topMargin: 4,
children: [
for (final Contact contact in contacts)
CupertinoListTile(
padding: EdgeInsets.zero,
title: Text('${contact.firstName} ${contact.lastName}'),
),
],
),
],
),
);
}
}
四、本节知识点小结
Sliver: Flutter 中专为滚动设计的特殊 Widget。只能放在 CustomScrollView 等滚动视图中。普通 Widget 需要用 SliverToBoxAdapter 或 SliverFillRemaining 包裹后才能在 Sliver 上下文中使用。
CustomScrollView: Sliver 的容器,通过 slivers 列表组合多个 Sliver,实现复杂的滚动效果。
CupertinoSliverNavigationBar: iOS 风格的可折叠导航栏。滚动时大标题自动折叠成小标题。.search 构造函数还能集成搜索框。
SliverList: Sliver 版本的列表。SliverList.list 接收普通 Widget 列表,自动将它们变成可滚动内容。
字母索引: 利用 ContactGroup.alphabetizedContacts 按姓氏首字母分组,每组一个 ContactListSection,实现 iOS 通讯录风格的字母索引。
五、下一步学习
联系人列表已经能滚动、能搜索、按字母分组了!下一课是本系列的最后一课——Stack Based Navigation(基于栈的导航),让小屏幕上点击分组能跳转到联系人列表页面。
我们下篇文章见!