← 返回笔记首页

Design System 架构方法论

多端共享组件库的设计哲学、组织方式与业界最佳实践 · 更新于 2026-02-07

目录
1. Design System 到底是什么 2. Apple 的方法:扁平化组件库 3. Google Material Design:Token → Theme → Component 4. Atomic Design:原子化分层体系 5. 三种方法的对比与取舍 6. 业界案例:Shopify、阿里、Airbnb 7. Flutter 特有的 ThemeExtension 模式 8. 反模式:Design System 的常见坑 9. Celoria 项目的选择与理由 10. 演进路线:从简单到成熟 11. 延伸阅读

1. Design System 到底是什么

Design System(设计系统)不只是一个组件库。它是一套完整的标准体系,包括:

层次内容类比
设计原则品牌调性、视觉语言、交互规范宪法
设计 Token颜色、间距、字体、圆角、阴影等具体数值法律条文
组件库可复用的 UI 组件(按钮、卡片、对话框…)执法机构
文档 & 治理使用指南、贡献规则、版本管理判例与司法解释
核心理念

Design System 的目的不是"让所有页面长一样",而是让一致性成为默认结果,而非额外努力。好的设计系统让开发者"做对的事比做错的事更容易"。

Design Token 是什么?

Design Token 是设计系统中最小的原子级决策。它把"设计师说的颜色"翻译成"开发者用的变量":

// 设计师的 Figma 标注
Primary Gold: #C89B3C
Body Spacing: 16px

// 开发者的代码
static const Color brandGold = Color(0xFFC89B3C);
static const double spacingMd = 16.0;

Token 的关键价值:单点修改,全端生效。改一次 tokens.json,Flutter 三端 + Web 端同步更新。

2. Apple 的方法:扁平化组件库

UIKit SwiftUI

核心架构

Foundation (日期、字符串、网络等基础工具)
    ↓
UIKit / SwiftUI (所有 UI 组件,扁平放置)
    ↓
App 层 (开发者用组件搭建页面)

关键特征

为什么 Apple 不分层?

Apple 的逻辑

Apple 认为开发者不应该关心"这个组件在体系中的层级",只需要关心"我需要什么功能"。 ButtonNavigationBar 对开发者来说都是"一个组件",强制区分原子/分子反而增加认知负担。

这个方法之所以 work,是因为 Apple 只有一个平台方在维护这个体系。当你的团队也很小时,这种扁平结构是最实用的。

优缺点

优点缺点
认知负担最低 — 不用想"这算原子还是分子"组件多了之后难找 — 需要好的搜索和文档
入门快 — 新人 import 一个包就能开始没有强制的组合规则 — 可能出现不一致的组合方式
组件间无依赖约束 — 任何组件可以用在任何地方大团队容易各自造轮子 — 缺少层级约束

3. Google Material Design:Token → Theme → Component

Material 3 Flutter

核心架构

Design Tokens (颜色原语、间距、字体)
    ↓
Theme System (ColorScheme + ThemeData + ThemeExtension)
    ↓
Material Components (ElevatedButton, Card, AppBar...)
    ↓
App 层 (组装页面)

关键特征

Material 3 的 Token 分层

Reference Tokens (最底层原语)
├── md.ref.palette.primary40 = #6750A4
├── md.ref.palette.neutral90 = #E6E1E5
└── md.ref.typeface.brand = Roboto

     ↓ 映射

System Tokens (语义化)
├── md.sys.color.primary = md.ref.palette.primary40
├── md.sys.color.on-primary = md.ref.palette.primary100
└── md.sys.typescale.body-large = 16sp / Roboto

     ↓ 映射

Component Tokens (组件级)
├── md.comp.filled-button.container.color = md.sys.color.primary
├── md.comp.filled-button.label-text.color = md.sys.color.on-primary
└── md.comp.filled-button.container.shape = md.sys.shape.corner.full
为什么要三层 Token?

这种设计让你可以在不同层级做修改:

优缺点

优点缺点
主题切换能力强 — 动态换肤/暗色模式开箱即用学习曲线陡峭 — 三层 Token + Theme 系统概念多
组件与颜色解耦 — 真正的"配置驱动"过度设计风险 — 小项目不需要三层 Token
Google 官方维护 — 生态完善、文档齐全定制困难 — 想跳出 Material 风格很痛苦

4. Atomic Design:原子化分层体系

Brad Frost Web 起源

五层结构

Atoms(原子)— 最小的不可再分组件
  Button, Input, Label, Icon, Avatar
      ↓ 组合
Molecules(分子)— 原子的功能组合
  SearchBar = Input + Button + Icon
  FormField = Label + Input + ErrorText
      ↓ 组合
Organisms(有机体)— 分子组成的独立区域
  Header = Logo + NavBar + SearchBar + Avatar
  DataTable = TableHeader + TableRow[] + Pagination
      ↓ 填入
Templates(模板)— 页面级布局结构(无真实数据)
  DashboardLayout = Sidebar + Header + ContentArea
      ↓ 填入
Pages(页面)— 填充真实数据的最终页面
  HomePage = DashboardLayout + 真实用户数据

核心哲学

Brad Frost 受化学启发:就像原子构成分子、分子构成有机体,UI 组件也应该从最小单位开始组合。关键是单向依赖

实际项目中的问题

Atomic Design 的痛点

优缺点

优点缺点
强制的组合纪律 — 大团队保持一致性分类争议 — "这是 atom 还是 molecule" 的无尽讨论
从小到大构建 — 确保基础组件被复用过度工程 — 5 层分级对中小团队太重
业界知名 — 很多设计师熟悉这套语言Flutter 适配差 — Widget 树本身就是组合式的

5. 三种方法的对比与取舍

维度 Apple Material Atomic
组件组织 扁平,按功能命名 按 Material 规范分类 5 层严格分级
样式传递 Environment / modifier 链 ThemeData + ColorScheme 不规定(各自实现)
学习成本 🟢 低 🟡 中 🟡 中(分类争议)
适合团队 1-5 人 任意规模 10+ 人大团队
主题切换 需要自建 🟢 开箱即用 需要自建
Flutter 适配 🟢 天然契合 🟢 官方支持 🟡 需要适配
典型代表 UIKit, SwiftUI Material, Fluent UI Shopify Polaris, Atlassian ADG
选择建议

6. 业界案例

Shopify Polaris

Shopify 的开源设计系统,服务于数千名开发者:

Alibaba / Meituan Flutter 实践

Airbnb DLS (Design Language System)

共同规律

这些公司最终都走向了类似的结构:Token + 扁平组件库 + Theme 系统。Atomic Design 作为设计沟通语言有价值,但作为代码组织方式,成熟的团队往往会简化甚至放弃它。

7. Flutter 特有的 ThemeExtension 模式

Flutter 3.0 引入了 ThemeExtension<T>,解决了 Material 的 ColorScheme 不够用的问题。

问题

Material 的 ColorScheme 只有 ~30 个颜色槽位(primary, secondary, surface…)。但实际项目经常需要更多语义颜色:打卡状态色、评分星星色、品牌渐变色… 这些塞不进 ColorScheme

解决方案:ThemeExtension

// 定义自定义 Token 集合
@immutable
class CeloriaBrandColors extends ThemeExtension<CeloriaBrandColors> {
  final Color brandPrimary;
  final Color brandAccent;
  final Color statusClockIn;
  final Color statusClockOut;
  final Color ratingStar;

  const CeloriaBrandColors({
    required this.brandPrimary,
    required this.brandAccent,
    required this.statusClockIn,
    required this.statusClockOut,
    required this.ratingStar,
  });

  @override
  CeloriaBrandColors copyWith({...}) => CeloriaBrandColors(...);

  @override
  CeloriaBrandColors lerp(CeloriaBrandColors? other, double t) {
    return CeloriaBrandColors(
      brandPrimary: Color.lerp(brandPrimary, other?.brandPrimary, t)!,
      // ... lerp 支持动画过渡
    );
  }
}

// 注册到 ThemeData
ThemeData(
  extensions: [
    CeloriaBrandColors(
      brandPrimary: CeloriaTokens.brandGold,
      brandAccent: CeloriaTokens.brandCocoaBrown,
      statusClockIn: CeloriaTokens.statusClockedIn,
      statusClockOut: CeloriaTokens.statusClockedOut,
      ratingStar: Color(0xFFFBBF24),
    ),
  ],
)

// 组件中使用
final brand = Theme.of(context).extension<CeloriaBrandColors>()!;
Container(color: brand.brandPrimary)

ThemeExtension 的价值

能力说明
类型安全编译时检查,不会拼错属性名
动画支持lerp() 方法让主题切换可以有过渡动画
多品牌Guest/Employee/Kiosk 各注册不同的 extension 实例
与 Material 共存不替换 ColorScheme,而是补充它

Theme 工厂模式

结合 Token + ThemeExtension,可以构建一个主题工厂,三端共用:

// celoria_ui/lib/theme/celoria_theme.dart
class CeloriaTheme {
  static ThemeData build({
    required Color primary,
    Color? secondary,
    Color? background,
    Color? surface,
    Color? error,
    Color? success,
    Brightness brightness = Brightness.light,
  }) {
    final scheme = ColorScheme.fromSeed(
      seedColor: primary,
      brightness: brightness,
    ).copyWith(
      primary: primary,
      secondary: secondary,
      surface: surface ?? CeloriaTokens.neutralSurface,
      error: error ?? CeloriaTokens.semanticError,
    );

    return ThemeData(
      useMaterial3: true,
      colorScheme: scheme,
      scaffoldBackgroundColor: background ?? CeloriaTokens.neutralBackground,
      extensions: [
        CeloriaBrandColors(brandPrimary: primary, ...),
      ],
      // ... 共享的按钮样式、卡片样式、输入框样式
    );
  }
}

// Guest App
theme: CeloriaTheme.build(
  primary: CeloriaTokens.brandGold,
  secondary: CeloriaTokens.brandCocoaBrown,
)

// Employee App
theme: CeloriaTheme.build(
  primary: CeloriaTokens.brandIndigo,
  background: CeloriaEmployeeTokens.neutralBackground,
  error: CeloriaEmployeeTokens.semanticError,
)

// Kiosk App
theme: CeloriaTheme.build(
  primary: CeloriaTokens.brandTeal,
  secondary: CeloriaTokens.brandCoral,
)

8. 反模式:Design System 的常见坑

组件直接引用 Token

✘ 错误做法

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        // 组件直接耦合 token 常量
        backgroundColor: CeloriaTokens.brandGold,
        foregroundColor: CeloriaTokens.textOnPrimary,
      ),
      ...
    );
  }
}

换主题时每个组件都要改

✔ 正确做法

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 从 Theme 获取,不直接引用 Token
    return ElevatedButton(
      // 样式由 ThemeData 统一控制
      // 组件本身不关心具体颜色
      onPressed: onPressed,
      child: child,
    );
  }
}

换主题只需改 ThemeData

更多反模式

反模式为什么有害正确做法
在共享库里放业务逻辑 共享库应该是纯 UI,混入 API 调用会创造不必要的依赖 共享库只负责展示,业务逻辑留在 App 层
全局静态变量管理主题 AppTheme.primaryColor 这种静态变量在 Widget rebuild 时不会触发更新 通过 Theme.of(context) 获取,确保响应式
过早抽象 一个只用了一次的组件没必要提到共享库 Rule of Three:至少 2 个 App 都需要时才提取
不做 breaking change 管理 改了共享组件的接口,3 个 App 同时报错 语义化版本 + deprecation 标记 + 渐进迁移
忽略响应式 手机上好看的组件在 iPad(Kiosk)上可能太小 组件接受尺寸参数,或用 LayoutBuilder 自适应

9. Celoria 项目的选择与理由

我们选择:Apple 扁平式 + Material Theme 系统

tokens.json (Single Source of Truth)
    ↓ generate.js
CeloriaTokens / CeloriaGuestTokens / CeloriaEmployeeTokens / CeloriaKioskTokens
    ↓
celoria_ui/
├── tokens/celoria_tokens.dart      # 自动生成,不手动编辑
├── theme/celoria_theme.dart        # ThemeData 工厂 (待建)
└── components/                     # 扁平组件库,按功能命名
    ├── loading_indicator.dart
    ├── error_view.dart
    ├── empty_state.dart
    ├── toast.dart
    ├── selectable_chip.dart         # 待建
    ├── selectable_card.dart         # 待建
    ├── pin_input.dart               # 待建
    ├── settings_menu_item.dart      # 待建
    └── tappable_contact.dart        # 待建

为什么这样选

决策理由
扁平不分层 1 人团队,10 个组件不需要 atoms/molecules/organisms 三个目录。以后超过 30 个可以再分
保留 ThemeData 三端品牌色不同(Gold/Indigo/Teal),ThemeData 工厂是最优解
分层 Token Guest 暖色调 vs Employee 冷色调是有意的设计区分,不应强制统一。共享层 + Override 层是正确做法
组件通过 Theme 取值 celoria_ui 的共享组件不直接引用 CeloriaTokens.brandGold,而是从 Theme.of(context) 取值,确保在不同 App 里自动适配品牌色
App 层保留自己的 AppTheme 各 App 的 app_theme.dart 作为"胶水层",调用 CeloriaTheme.build() 并添加 App 特有的样式(如 Kiosk 的大按钮)

重复组件审计结果

组件GuestEmployeeKioskceloria_ui
LoadingIndicator✅ 本地MaterialMaterial✅ 有但没人用
ErrorView✅ 本地MaterialMaterial✅ 有但没人用
EmptyState✅ 本地✅ 有但没人用
Toast✅ CenteredToastSnackBarSnackBar✅ 有但没人用
SelectableChip✅ 时间/日期私有 _步骤组件❌ 待建
SelectableCard✅ 技师选择私有 _TouchCard❌ 待建
PinInput✅ PinInputWidget私有 _NumPad❌ 待建
SettingsMenuItem✅ SettingsMenuItem❌ 待建

10. 演进路线:从简单到成熟

Phase 0 — 值统一 ✅ 已完成

tokens.json → 生成 Dart Token → 三端 app_theme.dart 引用 Token 而不是硬编码 hex 值

Phase 1 — Theme 工厂 + 已有组件替换

Phase 2 — 提取高频共享组件

Phase 3 — 新功能直接用共享库开发

Phase 4 — 工具链成熟化(可选)

关键原则:渐进式,不要一步到位

设计系统是种出来的,不是设计出来的(grown, not designed)。先解决最痛的重复问题,逐步扩展。追求一步到位的完美设计系统是最常见的失败模式。

11. 延伸阅读

书籍与文章

开源设计系统参考

Flutter 特定资源