高级 UI 特性
高级 UI 特性的温和入门:自适应布局、sliver、滚动、导航。
预览你将构建的 Rolodex 应用,并搭建基于 Cupertino 的项目与数据模型。
你将完成的内容
步骤
1
简介
简介
在 Flutter 教程系列的第三部中,你将使用 Flutter 的 Cupertino 库构建 iOS 通讯录应用的部分克隆版。
学完本教程后,你将学会如何创建自适应布局、实现全面的主题、构建导航模式,以及使用高级滚动技术。
你将学习的内容
#本教程探讨以下主题:
-
使用
LayoutBuilder构建响应式布局。 使用 sliver 和搜索实现高级滚动。
实现基于堆栈的导航模式。
-
使用
CupertinoThemeData创建全面的主题。 支持浅色和深色主题。
使用 Cupertino widget 创建 iOS 风格 UI。
本教程假定你已完成之前的 Flutter 教程,并熟悉基本的 widget 组合、状态管理以及 Flutter 项目结构。
2
创建新的 Flutter 项目
创建新的 Flutter 项目
要构建 Flutter 应用,你首先需要一个 Flutter 项目。你可以使用随 Flutter SDK 一起安装的 Flutter CLI 工具 创建新应用。
打开你偏好的终端并运行以下命令以创建新的 Flutter 项目:
flutter create rolodex --empty
cd rolodex
此命令会创建一个使用精简「empty」模板的新 Flutter 项目。
3
添加 Cupertino Icons 依赖
添加 Cupertino Icons 依赖
此项目使用 cupertino_icons package,这是一个官方 Flutter package。通过运行以下命令将其添加为依赖:
flutter pub add cupertino_icons
4
搭建项目结构
搭建项目结构
首先,为应用创建基本目录结构。在项目的 lib 目录中,创建以下文件夹:
mkdir lib/data lib/screens lib/theme
此命令会创建文件夹,将代码组织为逻辑分区:数据模型、屏幕 widget 和主题配置。
5
替换入门代码
替换入门代码
在 IDE 中打开 lib/main.dart 文件,并将其全部内容替换为以下入门代码:
import 'package:flutter/cupertino.dart';
void main() {
runApp(const RolodexApp());
}
class RolodexApp extends StatelessWidget {
const RolodexApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
title: 'Rolodex',
theme: CupertinoThemeData(
barBackgroundColor: CupertinoDynamicColor.withBrightness(
color: Color(0xFFF9F9F9),
darkColor: Color(0xFF1D1D1D),
),
),
home: CupertinoPageScaffold(child: Center(child: Text('Hello Rolodex!'))),
);
}
}
与之前两个教程不同,此应用使用 CupertinoApp 而非 MaterialApp。
Cupertino 设计系统提供 iOS 风格的 widget 和样式,非常适合构建在 Apple 设备上具有原生体验的应用。
6
运行应用
运行应用
在 Flutter 应用根目录的终端中,运行以下命令:
flutter run -d chrome
应用会在新的 Chrome 实例中构建并启动。它会在屏幕中央显示「Hello Rolodex!」。
7
创建数据模型
创建数据模型
在构建 UI 之前,创建应用将使用的数据结构和示例数据。本节仅作简要说明,因为不是本教程的重点。
联系人数据
#创建新文件 lib/data/contact.dart,并添加基本的 Contact 类:
class Contact {
Contact({
required this.id,
required this.firstName,
this.middleName,
required this.lastName,
this.suffix,
});
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 davidTaylor = Contact(id: 4, firstName: 'David', lastName: 'Taylor');
final hankZakroff = Contact(
id: 5,
firstName: 'Hank',
middleName: 'M.',
lastName: 'Zakroff',
);
final alexAnderson = Contact(id: 6, firstName: 'Alex', lastName: 'Anderson');
final benBrown = Contact(id: 7, firstName: 'Ben', lastName: 'Brown');
final carolCarter = Contact(id: 8, firstName: 'Carol', lastName: 'Carter');
final dianaDevito = Contact(id: 9, firstName: 'Diana', lastName: 'Devito');
final emilyEvans = Contact(id: 10, firstName: 'Emily', lastName: 'Evans');
final frankFisher = Contact(id: 11, firstName: 'Frank', lastName: 'Fisher');
final graceGreen = Contact(id: 12, firstName: 'Grace', lastName: 'Green');
final henryHall = Contact(id: 13, firstName: 'Henry', lastName: 'Hall');
final isaacIngram = Contact(id: 14, firstName: 'Isaac', lastName: 'Ingram');
final juliaJackson = Contact(id: 15, firstName: 'Julia', lastName: 'Jackson');
final kevinKelly = Contact(id: 16, firstName: 'Kevin', lastName: 'Kelly');
final lindaLewis = Contact(id: 17, firstName: 'Linda', lastName: 'Lewis');
final michaelMiller = Contact(id: 18, firstName: 'Michael', lastName: 'Miller');
final nancyNewman = Contact(id: 19, firstName: 'Nancy', lastName: 'Newman');
final oliverOwens = Contact(id: 20, firstName: 'Oliver', lastName: 'Owens');
final penelopeParker = Contact(
id: 21,
firstName: 'Penelope',
lastName: 'Parker',
);
final quentinQuinn = Contact(id: 22, firstName: 'Quentin', lastName: 'Quinn');
final rachelReed = Contact(id: 23, firstName: 'Rachel', lastName: 'Reed');
final samuelSmith = Contact(id: 24, firstName: 'Samuel', lastName: 'Smith');
final tessaTurner = Contact(id: 25, firstName: 'Tessa', lastName: 'Turner');
final umbertoUpton = Contact(id: 26, firstName: 'Umberto', lastName: 'Upton');
final victoriaVance = Contact(id: 27, firstName: 'Victoria', lastName: 'Vance');
final williamWilson = Contact(id: 28, firstName: 'William', lastName: 'Wilson');
final xavierXu = Contact(id: 29, firstName: 'Xavier', lastName: 'Xu');
final yasmineYoung = Contact(id: 30, firstName: 'Yasmine', lastName: 'Young');
final zacharyZimmerman = Contact(
id: 31,
firstName: 'Zachary',
lastName: 'Zimmerman',
);
final elizabethMJohnson = Contact(
id: 32,
firstName: 'Elizabeth',
middleName: 'M.',
lastName: 'Johnson',
);
final robertLWilliamsSr = Contact(
id: 33,
firstName: 'Robert',
middleName: 'L.',
lastName: 'Williams',
suffix: 'Sr.',
);
final margaretAnneDavis = Contact(
id: 34,
firstName: 'Margaret',
middleName: 'Anne',
lastName: 'Davis',
);
final williamJamesBrownIII = Contact(
id: 35,
firstName: 'William',
middleName: 'James',
lastName: 'Brown',
suffix: 'III',
);
final maryElizabethClark = Contact(
id: 36,
firstName: 'Mary',
middleName: 'Elizabeth',
lastName: 'Clark',
);
final drSarahWatson = Contact(
id: 37,
firstName: 'Dr. Sarah',
lastName: 'Watson',
);
final jamesRSmithEsq = Contact(
id: 38,
firstName: 'James',
middleName: 'R.',
lastName: 'Smith',
suffix: 'Esq.',
);
final mariaCruz = Contact(id: 39, firstName: 'Maria', lastName: 'Cruz');
final pierreMartin = Contact(id: 40, firstName: 'Pierre', lastName: 'Martin');
final yukiTanaka = Contact(id: 41, firstName: 'Yuki', lastName: 'Tanaka');
final hansSchmidt = Contact(id: 42, firstName: 'Hans', lastName: 'Schmidt');
final priyaPatel = Contact(id: 43, firstName: 'Priya', lastName: 'Patel');
final carlosGarcia = Contact(id: 44, firstName: 'Carlos', lastName: 'Garcia');
final ninaVolkova = Contact(id: 45, firstName: 'Nina', lastName: 'Volkova');
final jenniferAdams = Contact(id: 46, firstName: 'Jennifer', lastName: 'Adams');
final michaelBaker = Contact(id: 47, firstName: 'Michael', lastName: 'Baker');
final sarahCooper = Contact(id: 48, firstName: 'Sarah', lastName: 'Cooper');
final christopherDaniel = Contact(
id: 49,
firstName: 'Christopher',
lastName: 'Daniel',
);
final jessicaEdwards = Contact(
id: 50,
firstName: 'Jessica',
lastName: 'Edwards',
);
final Set<Contact> allContacts = {
johnAppleseed,
kateBell,
annaHaro,
danielHiggins,
davidTaylor,
hankZakroff,
alexAnderson,
benBrown,
carolCarter,
dianaDevito,
emilyEvans,
frankFisher,
graceGreen,
henryHall,
isaacIngram,
juliaJackson,
kevinKelly,
lindaLewis,
michaelMiller,
nancyNewman,
oliverOwens,
penelopeParker,
quentinQuinn,
rachelReed,
samuelSmith,
tessaTurner,
umbertoUpton,
victoriaVance,
williamWilson,
xavierXu,
yasmineYoung,
zacharyZimmerman,
elizabethMJohnson,
robertLWilliamsSr,
margaretAnneDavis,
williamJamesBrownIII,
maryElizabethClark,
drSarahWatson,
jamesRSmithEsq,
mariaCruz,
pierreMartin,
yukiTanaka,
hansSchmidt,
priyaPatel,
carlosGarcia,
ninaVolkova,
jenniferAdams,
michaelBaker,
sarahCooper,
christopherDaniel,
jessicaEdwards,
};
此示例数据包含带和不带中间名、后缀的联系人。这为你构建 UI 时提供了多种数据可供使用。
ContactGroup 数据
#
现在,创建将联系人组织成列表的分组。创建新的 lib/data/contact_group.dart 文件并添加 ContactGroup 类:
import 'dart:collection';
import 'package:flutter/cupertino.dart';
import 'contact.dart';
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;
AlphabetizedContactMap get alphabetizedContacts {
final contactsMap = AlphabetizedContactMap();
for (final contact in _contacts) {
final lastInitial = contact.lastName[0].toUpperCase();
if (contactsMap.containsKey(lastInitial)) {
contactsMap[lastInitial]!.add(contact);
} else {
contactsMap[lastInitial] = [contact];
}
}
return contactsMap;
}
}
ContactGroup 表示一组联系人,例如「All Contacts」或「Favorites」。
将以下辅助代码和示例数据添加到 lib/data/contact_group.dart:
typedef AlphabetizedContactMap = SplayTreeMap<String, List<Contact>>;
/// Sorts a list of [contacts] alphabetically by
/// last name, then first name, then middle name.
/// If names are identical, sorts by contact ID to ensure consistent ordering.
void _sortContacts(List<Contact> contacts) {
contacts.sort((a, b) {
final checkLastName = a.lastName.compareTo(b.lastName);
if (checkLastName != 0) {
return checkLastName;
}
final checkFirstName = a.firstName.compareTo(b.firstName);
if (checkFirstName != 0) {
return checkFirstName;
}
if (a.middleName != null && b.middleName != null) {
final 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;
}
// If both contacts have the exact same name, order by first created.
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];
}
此代码创建三个示例分组和一个函数,用于生成应用的初始数据。
最后,在 lib/data/contact_group.dart 中添加管理状态变化的类:
class ContactGroupsModel {
ContactGroupsModel() : _listsNotifier = ValueNotifier(generateSeedData());
final ValueNotifier<List<ContactGroup>> _listsNotifier;
ValueNotifier<List<ContactGroup>> get listsNotifier => _listsNotifier;
List<ContactGroup> get lists => _listsNotifier.value;
ContactGroup findContactList(int id) {
return lists[id];
}
void dispose() {
_listsNotifier.dispose();
}
}
如果你不熟悉 ValueNotifier,应先完成 涵盖状态的上一篇教程
再继续,该教程涵盖状态管理。
8
将数据连接到应用
将数据连接到应用
更新 main.dart 以包含全局状态并导入新的数据文件:
import 'package:flutter/cupertino.dart';
import 'data/contact_group.dart';
final contactGroupsModel = ContactGroupsModel();
void main() {
runApp(const RolodexApp());
}
class RolodexApp extends StatelessWidget {
const RolodexApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
title: 'Rolodex',
theme: CupertinoThemeData(
barBackgroundColor: CupertinoDynamicColor.withBrightness(
color: Color(0xFFF9F9F9),
darkColor: Color(0xFF1D1D1D),
),
),
home: CupertinoPageScaffold(child: Center(child: Text('Hello Rolodex!'))),
);
}
}
清理完所有多余代码后,在下一课中,你将正式开始构建应用。
9
回顾
回顾
你完成的内容
以下是你本课构建与学习内容的摘要。预览了 Rolodex 应用
你正在开始一个专注于高级 UI 特性的新教程章节。为了让应用在任何设备上都感觉精致且原生,你将学习自适应布局、sliver、导航和主题。
使用 Cupertino widget 搭建了项目
与之前的课程不同,此应用使用 CupertinoApp 而非 MaterialApp。 Cupertino 设计系统提供在 Apple 设备上具有原生体验的 iOS 风格 widget。
为联系人和分组创建了数据模型
你创建了带示例数据的 Contact 和 ContactGroup 类,以及用于状态管理的 ContactGroupsModel。这一基础将支持你在后续课程中构建的 UI。
10
自测
自测
高级 UI 搭建测验
1 / 2-
CupertinoApp 只能在 iOS 设备上运行。
不正确。
CupertinoApp 可在任何平台上运行;它只是提供 iOS 风格的 widget。
-
CupertinoApp 提供 iOS 风格的 widget 和样式,而 MaterialApp 提供 Material Design widget。
正确!
CupertinoApp 使用与 iOS 外观和体验相匹配的 Cupertino 设计系统 widget。
-
CupertinoApp 更轻量且性能更好。
不正确。
两者性能相近;区别在于视觉风格,而非速度。
-
MaterialApp 需要更多配置才能搭建。
不正确。
两者搭建要求相近;只是使用不同的设计系统。
-
验证用户输入值。
不正确。
ValueNotifier 持有值并在值变化时通知,而非用于验证。
-
持有一个值,并在该值变化时通知监听者。
正确!
ValueNotifier 是一个简单的 ChangeNotifier,它包装单个值并在变化时通知监听者。
-
在不同数据类型之间转换值。
不正确。
类型转换不是 ValueNotifier 的用途。
-
将值永久存储在本地存储中。
不正确。
ValueNotifier 在内存中持有值;持久化需要单独实现。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-18。查看文档源码 或者 为本页面内容提出建议。