跳转至正文

高级 UI 特性

高级 UI 特性的温和入门:自适应布局、sliver、滚动、导航。

预览你将构建的 Rolodex 应用,并搭建基于 Cupertino 的项目与数据模型。

你将完成的内容

预览你将构建的 Rolodex 应用
使用 Cupertino widget 搭建项目
为联系人和分组创建数据模型

步骤

1

简介

在 Flutter 教程系列的第三部中,你将使用 Flutter 的 Cupertino 库构建 iOS 通讯录应用的部分克隆版。

A screenshot of the completed Rolodex contact
management app showing a list of contacts organized alphabetically.

学完本教程后,你将学会如何创建自适应布局、实现全面的主题、构建导航模式,以及使用高级滚动技术。

你将学习的内容

#

本教程探讨以下主题:

  • 使用 LayoutBuilder 构建响应式布局。

  • 使用 sliver 和搜索实现高级滚动。

  • 实现基于堆栈的导航模式。

  • 使用 CupertinoThemeData 创建全面的主题。

  • 支持浅色和深色主题。

  • 使用 Cupertino widget 创建 iOS 风格 UI。

本教程假定你已完成之前的 Flutter 教程,并熟悉基本的 widget 组合、状态管理以及 Flutter 项目结构。

2

创建新的 Flutter 项目

要构建 Flutter 应用,你首先需要一个 Flutter 项目。你可以使用随 Flutter SDK 一起安装的 Flutter CLI 工具 创建新应用。

打开你偏好的终端并运行以下命令以创建新的 Flutter 项目:

flutter create rolodex --empty
cd rolodex

此命令会创建一个使用精简「empty」模板的新 Flutter 项目。

3

添加 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 文件,并将其全部内容替换为以下入门代码:

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 类:

dart
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 类:

dart
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

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 中添加管理状态变化的类:

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 以包含全局状态并导入新的数据文件:

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。

为联系人和分组创建了数据模型

你创建了带示例数据的 ContactContactGroup 类,以及用于状态管理的 ContactGroupsModel。这一基础将支持你在后续课程中构建的 UI。

10

自测

高级 UI 搭建测验

1 / 2
CupertinoApp 与 MaterialApp 的主要区别是什么?
  1. CupertinoApp 只能在 iOS 设备上运行。

    不正确。

    CupertinoApp 可在任何平台上运行;它只是提供 iOS 风格的 widget。

  2. CupertinoApp 提供 iOS 风格的 widget 和样式,而 MaterialApp 提供 Material Design widget。

    正确!

    CupertinoApp 使用与 iOS 外观和体验相匹配的 Cupertino 设计系统 widget。

  3. CupertinoApp 更轻量且性能更好。

    不正确。

    两者性能相近;区别在于视觉风格,而非速度。

  4. MaterialApp 需要更多配置才能搭建。

    不正确。

    两者搭建要求相近;只是使用不同的设计系统。

ValueNotifier 在状态管理中的作用是什么?
  1. 验证用户输入值。

    不正确。

    ValueNotifier 持有值并在值变化时通知,而非用于验证。

  2. 持有一个值,并在该值变化时通知监听者。

    正确!

    ValueNotifier 是一个简单的 ChangeNotifier,它包装单个值并在变化时通知监听者。

  3. 在不同数据类型之间转换值。

    不正确。

    类型转换不是 ValueNotifier 的用途。

  4. 将值永久存储在本地存储中。

    不正确。

    ValueNotifier 在内存中持有值;持久化需要单独实现。