前言
恭喜你完成了前两个项目:Birdle 猜词游戏和维基百科阅读器!你已经掌握了 Widget 基础、布局、状态管理和 MVVM 架构。
从这一篇开始,我们进入全新的章节------Flutter UI 102 。我们将构建第三个应用:一个 iOS 风格的通讯录应用 Rolodex。在这个过程中,你会学到自适应布局、高级滚动、导航模式和 iOS 风格主题等进阶 UI 技巧。
本文基于官方教程的「Advanced UI Features」章节,今天的任务是搭建 Rolodex 项目、认识 Cupertino 组件库,并创建数据模型。
一、新应用要做什么?
Rolodex 是一个仿 iOS 通讯录的应用,最终效果包括:
- 自适应布局:大屏幕显示侧边栏 + 详情面板,小屏幕用导航跳转
- 高级滚动:使用 Sliver 实现可折叠的搜索栏和字母索引
- 导航模式:基于栈的页面跳转(push/pop)
- iOS 风格主题:使用 Cupertino 组件,支持亮色/暗色模式
1.1 Material vs Cupertino
前两个项目用的都是 MaterialApp(Material Design 风格),这是 Google 的设计语言。而这次我们用 CupertinoApp(Cupertino 风格),这是 Apple 的 iOS 设计语言。
两者的核心区别:
less
// Material 风格(Google 设计语言)
// 前两个项目一直在用
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('标题')),
body: ...,
),
)
// Cupertino 风格(Apple iOS 设计语言)
// 本项目使用
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('标题')),
child: ...,
),
)
虽然组件名字不同,但编程方式完全一样------都是 Widget 嵌套。Cupertino 组件在所有平台上都能运行(不只是 iOS),只是视觉风格模仿了 iOS。
二、创建 Rolodex 项目
2.1 创建项目并安装依赖
bash
# 创建新项目
flutter create rolodex --empty
# 进入项目目录
cd rolodex
# 安装 Cupertino 图标包
# 提供了 iOS 风格的图标(如返回箭头、搜索图标等)
flutter pub add cupertino_icons
# 创建代码目录结构
# data/ → 数据模型
# screens/ → 页面组件
# theme/ → 主题配置
mkdir lib/data lib/screens lib/theme
2.2 替换 main.dart
scala
// 导入 Cupertino 组件库(替代 Material)
// Cupertino 提供了 iOS 风格的按钮、导航栏、列表等组件
import 'package:flutter/cupertino.dart';
// 导入数据模型(下一步创建)
import 'package:rolodex/data/contact_group.dart';
// 全局状态:联系人分组的数据模型
// 使用 ValueNotifier 管理状态变化
final contactGroupsModel = ContactGroupsModel();
void main() {
runApp(const RolodexApp());
}
// RolodexApp:应用根组件
// 使用 CupertinoApp 替代 MaterialApp
class RolodexApp extends StatelessWidget {
const RolodexApp({super.key});
@override
Widget build(BuildContext context) {
// CupertinoApp 提供 iOS 风格的应用框架
// 包括 iOS 风格的路由动画、字体、颜色等
return CupertinoApp(
title: 'Rolodex',
// CupertinoThemeData 配置 iOS 风格主题
theme: const CupertinoThemeData(
// barBackgroundColor 设置导航栏的背景色
// CupertinoDynamicColor 自动适配亮色/暗色模式
barBackgroundColor: CupertinoDynamicColor.withBrightness(
color: Color(0xFFF9F9F9), // 亮色模式:浅灰白
darkColor: Color(0xFF1D1D1D), // 暗色模式:深灰黑
),
),
// CupertinoPageScaffold 是 iOS 风格的页面脚手架
// 相当于 Material 的 Scaffold
home: CupertinoPageScaffold(
child: Center(child: Text('Hello Rolodex!')),
),
);
}
}
三、创建数据模型
3.1 Contact 类
在 lib/data/contact.dart 中创建联系人数据模型:
dart
// Contact:单个联系人
// 包含 ID、姓、名、中间名(可选)、后缀(可选,如 Jr.、Sr.)
class Contact {
Contact({
required this.id, // 唯一标识符
required this.firstName, // 名
this.middleName, // 中间名(可选)
required this.lastName, // 姓
this.suffix, // 后缀(可选,如 Jr.、III)
});
final int id;
final String firstName;
final String lastName;
final String? middleName; // ? 表示可空
final String? suffix;
}
// 示例联系人数据
final johnAppleseed = Contact(id: 0, firstName: 'John', lastName: 'Appleseed');
final kateBell = Contact(id: 1, firstName: 'Kate', lastName: 'Bell');
final annaHaro = Contact(id: 2, firstName: 'Anna', lastName: 'Haro');
final danielHiggins = Contact(
id: 3, firstName: 'Daniel', lastName: 'Higgins', suffix: 'Jr.',
);
// ... 更多联系人(完整列表见代码库文件)
// 所有联系人的集合
final Set<Contact> allContacts = <Contact>{
johnAppleseed, kateBell, annaHaro, danielHiggins,
// ... 完整列表
};
3.2 ContactGroup 类
在 lib/data/contact_group.dart 中创建联系人分组:
dart
import 'dart:collection';
import 'package:flutter/cupertino.dart';
import 'contact.dart';
// ContactGroup:联系人分组
// 如"所有联系人"、"收藏"、"工作"等
class ContactGroup {
factory ContactGroup({
required int id,
required String label,
bool permanent = false, // 是否为固定分组(不可删除)
String? title,
List<Contact>? contacts,
}) {
// 创建时自动按姓名排序
final contactsCopy = contacts ?? <Contact>[];
_sortContacts(contactsCopy);
return ContactGroup._internal(
id: id, label: label, permanent: permanent,
title: title, contacts: contactsCopy,
);
}
// 私有构造函数
ContactGroup._internal({
required this.id,
required this.label,
this.permanent = false,
String? title,
List<Contact>? contacts,
}) : title = title ?? label,
_contacts = contacts ?? const <Contact>[];
final int id;
final bool permanent;
final String label;
final String title;
final List<Contact> _contacts;
List<Contact> get contacts => _contacts;
// 按首字母分组(用于字母索引滚动列表)
// SplayTreeMap 自动按 key 排序(A、B、C...)
AlphabetizedContactMap get alphabetizedContacts {
final AlphabetizedContactMap contactsMap = AlphabetizedContactMap();
for (final Contact contact in _contacts) {
final String lastInitial = contact.lastName[0].toUpperCase();
if (contactsMap.containsKey(lastInitial)) {
contactsMap[lastInitial]!.add(contact);
} else {
contactsMap[lastInitial] = [contact];
}
}
return contactsMap;
}
}
// 按字母排序的联系人 Map 类型别名
typedef AlphabetizedContactMap = SplayTreeMap<String, List<Contact>>;
// 联系人排序函数:先按姓排序,再按名,再按中间名
void _sortContacts(List<Contact> contacts) {
contacts.sort((Contact a, Contact b) {
final int checkLastName = a.lastName.compareTo(b.lastName);
if (checkLastName != 0) return checkLastName;
final int checkFirstName = a.firstName.compareTo(b.firstName);
if (checkFirstName != 0) return checkFirstName;
if (a.middleName != null && b.middleName != null) {
final int checkMiddleName = a.middleName!.compareTo(b.middleName!);
if (checkMiddleName != 0) return checkMiddleName;
} else if (a.middleName != null || b.middleName != null) {
return a.middleName != null ? 1 : -1;
}
return a.id.compareTo(b.id);
});
}
// 示例分组数据
final allPhone = ContactGroup(
id: 0, permanent: true,
label: 'All iPhone', title: 'iPhone',
contacts: allContacts.toList(),
);
final friends = ContactGroup(
id: 1, label: 'Friends',
contacts: [allContacts.elementAt(3)],
);
final work = ContactGroup(id: 2, label: 'Work');
// 生成初始数据
List<ContactGroup> generateSeedData() {
return [allPhone, friends, work];
}
// ContactGroupsModel:状态管理
// 使用 ValueNotifier 管理分组列表的变化
class ContactGroupsModel {
ContactGroupsModel()
: _listsNotifier = ValueNotifier(generateSeedData());
// ValueNotifier:一种简化版的 ChangeNotifier
// 它包裹一个值,当值改变时自动通知监听者
final ValueNotifier<List<ContactGroup>> _listsNotifier;
ValueNotifier<List<ContactGroup>> get listsNotifier => _listsNotifier;
List<ContactGroup> get lists => _listsNotifier.value;
// 根据 ID 查找分组
ContactGroup findContactList(int id) {
return lists[id];
}
// 释放资源
void dispose() {
_listsNotifier.dispose();
}
}
四、ValueNotifier 简介
在维基百科阅读器中我们用了 ChangeNotifier。这次用的 ValueNotifier 是它的简化版:
scala
// ChangeNotifier:通用版,可以有多个属性
// 需要手动调用 notifyListeners()
class ArticleViewModel extends ChangeNotifier {
Summary? summary;
bool loading = false;
// 修改后需要手动调用 notifyListeners()
}
// ValueNotifier:简化版,只包裹一个值
// 修改 .value 时自动通知监听者,不需要手动调用
final counter = ValueNotifier<int>(0);
counter.value = 1; // 自动通知!无需调用 notifyListeners()
当你的状态只有一个值时,ValueNotifier 比 ChangeNotifier 更简洁。
五、本节知识点小结
CupertinoApp: Apple iOS 风格的应用框架,替代 MaterialApp。提供 iOS 风格的导航栏、按钮、列表等组件。可在所有平台运行,不只是 iOS。
CupertinoDynamicColor: 能自动适配亮色和暗色模式的颜色类。传入两个颜色值,系统会根据当前模式自动选择。
ValueNotifier: ChangeNotifier 的简化版,包裹一个值。当 .value 被修改时自动通知监听者,不需要手动调用 notifyListeners()。
项目结构: 按职责划分目录------data/ 放数据模型、screens/ 放页面组件、theme/ 放主题配置,让代码组织更清晰。
六、下一步学习
项目骨架和数据模型已就绪。下一课我们将学习 自适应布局 (Adaptive Layouts),使用 LayoutBuilder 让应用在不同屏幕尺寸上自动切换布局方式。
我们下篇文章见!