進階 UI 功能
進階 UI 功能的溫和入門: 自適應版面配置、sliver、滾動、導覽。
預覽你將建置的 Rolodex 應用程式,並設定一個以 Cupertino 為基礎的專案與資料模型。
你將完成的事項
Steps
1
簡介
簡介
在這個 Flutter 教學系列的第三部分, 你將使用 Flutter 的 Cupertino 函式庫,打造一個 iOS 聯絡人 (Contacts) 應用程式的部分複製版。
完成本教學後,你將學會如何建立自適應版面配置、實作完整的主題化、 建構導覽模式,以及運用進階的滾動技術。
你將學到什麼
#本教學將探討以下主題:
- 使用
LayoutBuilder建立響應式版面配置。 - 運用 sliver 和搜尋實現進階滾動效果。
- 實作基於堆疊的導覽模式。
- 透過
CupertinoThemeData建立完整的主題。 - 同時支援淺色與深色主題。
- 使用 Cupertino 元件 (Widget) 打造 iOS 風格的 UI。
本教學假設你已完成前面的 Flutter 教學, 並且熟悉基本的元件 (Widget) 組合、狀態管理, 以及 Flutter 專案結構。
2
建立新的 Flutter 專案
建立新的 Flutter 專案
要開發 Flutter 應用程式,首先需要一個 Flutter 專案。 你可以使用 Flutter CLI 工具 (隨 Flutter SDK 一併安裝)來建立新的應用程式。
請開啟你偏好的終端機,執行以下指令來建立新的 Flutter 專案:
flutter create rolodex --empty
cd rolodex
此指令會建立一個使用最小化「空白」範本的新 Flutter 專案。
3
新增 Cupertino Icons 相依套件
新增 Cupertino Icons 相依套件
此專案會使用 cupertino_icons 套件,
這是一個官方的 Flutter 套件。
請透過執行下列指令,將其加入為相依套件:
flutter pub add cupertino_icons
4
建立專案結構
建立專案結構
首先,請為你的應用程式建立基本的目錄結構。
在你的專案 lib 目錄下,建立以下資料夾:
mkdir lib/data lib/screens lib/theme
此指令會建立資料夾,將你的程式碼依照邏輯區塊進行組織: 資料模型 (data models)、螢幕元件 (screen widgets),以及主題設定 (theme configuration)。
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 風格的元件與樣式,
非常適合打造在 Apple 裝置上具有原生體驗的應用程式。
6
執行你的應用程式
執行你的應用程式
在終端機中,於你的 Flutter 應用程式根目錄下執行以下指令:
flutter run -d chrome
應用程式會在一個新的 Chrome 實例中建置並啟動。 它會在螢幕中央顯示「Hello Rolodex!」。
7
建立資料模型
建立資料模型
在開始建立 UI 之前, 請先建立應用程式將會使用的資料結構與範例資料。 本節僅做簡要說明,因為這不是本教學的重點。
Contact 資料
#請建立一個新檔案 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 代表一組聯絡人,例如「所有聯絡人」或「我的最愛」。
請將以下輔助程式碼與範例資料加入 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];
}
這段程式碼建立了三個範例群組 (sample groups) 以及一個用於產生應用程式初始資料的函式。
最後,將一個管理狀態變化的類別加入 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 元件的專案
與前面的課程不同, 這個應用程式使用 CupertinoApp 而不是 MaterialApp。 Cupertino 設計系統提供 iOS 風格的元件, 在 Apple 裝置上具有原生體驗。
建立了聯絡人與群組的資料模型
你建立了含有範例資料的 Contact 和 ContactGroup 類別, 以及用於狀態管理的 ContactGroupsModel。 這個基礎支撐了你在後續課程中將建置的 UI。
10
自我測驗
自我測驗
進階 UI 設定測驗
1 / 2-
CupertinoApp 只能在 iOS 裝置上運作。
Not quite
CupertinoApp 可以在任何平台上執行;它只是提供 iOS 風格的元件。
-
CupertinoApp 更輕量且效能更好。
Not quite
兩者效能相近;它們的差異在於視覺風格,而不是速度。
-
CupertinoApp 提供 iOS 風格的元件與樣式,而 MaterialApp 提供 Material Design 元件。
That's right!
CupertinoApp 使用符合 iOS 外觀與體驗的 Cupertino 設計系統元件。
-
MaterialApp 需要更多設定才能完成配置。
Not quite
兩者的設定需求相近;它們只是使用不同的設計系統。
-
用來驗證使用者輸入的值。
Not quite
ValueNotifier 用於持有值並在值變更時通知監聽者,而不是驗證。
-
持有單一值,並在該值變更時通知監聽者。
That's right!
ValueNotifier 是一個簡單的 ChangeNotifier,它包裝單一值,並在值變更時通知監聽者。
-
將值永久儲存在本機儲存空間中。
Not quite
ValueNotifier 將值保存在記憶體中;持久化需要另外實作。
-
在不同資料型別之間轉換值。
Not quite
型別轉換不是 ValueNotifier 的用途。
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.