开篇:让设计系统在代码里「活起来」
设计师交付的不只是「几个颜色值」,而是一套设计系统。这套系统在代码里的落地方式,决定了大厂 UI 与散装 UI 的差距:
- 颜色不写死
#FF0000,而是用Theme.of(context).colorScheme.primary。 - 间距不靠拍脑袋
EdgeInsets.all(13),而是用 token 网格Spacing.lg。 - 深色模式不是「反色」,而是语义 token 的「同一套语义、另一组值」。
**Design Tokens(设计令牌)**就是连接设计语言与代码的桥梁。本文讲透:Token 怎么分类、怎么映射到 Flutter 的 ThemeData、怎么让主题自适应系统与平台——一套可扩展的主题工程。
一、Token 的分类体系
1.1 三级 Token
设计令牌按抽象层级分三类,越往上越「语义化」:
# 1) 全局 Token(原始值): 只描述"是什么"
# color.blue.500 #2196F3
# spacing.4 4
# radius.md 12
# 字体字号/字重基线
# 2) 语义 Token(表达"用在哪"): 引用全局值,描述意图
# color.primary → color.blue.500(品牌主色)
# color.surface → 卡片/页面背景
# color.text.primary → 正文文字
# spacing.cardPadding → spacing.4 * 4
# 3) 组件级 Token: 绑定到具体组件
# button.primaryBackground → color.primary
# button.primaryText → color.onPrimary
# card.radius → radius.md
为什么语义 Token 是关键:深色模式只换「语义 Token → 全局值」的映射,组件代码一行不用改。「语义」与「值」分离,是主题系统可维护的根基。
1.2 Token 命名规范
# 命名: 域.对象.属性.状态
# color.button.background.pressed
# spacing.container.padding
# typography.headline.size
# 一致性: 全部小写 + 点分,禁止拼音/缩写歧义
二、Token 映射到 ThemeData
2.1 代码里的 Token 落地
用 Dart 类承载 token,ThemeData 引用它:
// tokens.dart —— 全局值
abstract final class AppTokens {
static const spacing4 = 4.0;
static const spacing8 = 8.0;
static const radiusMd = 12.0;
// ...原始值
}
// semantic_tokens.dart —— 语义映射(亮/暗两套)
abstract final class LightSemantics {
static const primary = Color(0xFF2196F3);
static const surface = Color(0xFFFFFFFF);
static const textPrimary = Color(0xFF212121);
}
abstract final class DarkSemantics {
static const primary = Color(0xFF64B5F6);
static const surface = Color(0xFF121212);
static const textPrimary = Color(0xFFE0E0E0);
}
2.2 组装 ThemeData
ThemeData buildTheme(Brightness brightness) {
final s = brightness == Brightness.dark ? DarkSemantics() : LightSemantics();
final scheme = ColorScheme.fromSeed(
seedColor: s.primary,
brightness: brightness,
surface: s.surface,
);
return ThemeData(
colorScheme: scheme,
textTheme: _buildTextTheme(s),
cardTheme: CardThemeData(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTokens.radiusMd)),
),
);
}
核心思想:ThemeData 是一个「集中配置对象」,组件从 Theme.of(context) 读取,永不写死颜色。深色/品牌切换 = 换一份 ThemeData,整个 App 自动变。
三、深色模式的自适应
3.1 跟随系统的正确姿势
Flutter 默认 darkTheme + themeMode 三步走:
MaterialApp(
theme: buildTheme(Brightness.light),
darkTheme: buildTheme(Brightness.dark),
themeMode: ThemeMode.system, // 跟随系统
// 手动切换: 存偏好,设置 themeMode: light/dark/system
)
ThemeMode.system跟随系统设置,iOS 的「Auto」/ Android 的「深色模式」自动适配。- 手动切换:把偏好存进
shared_preferences,启动时读回决定 themeMode。
3.2 语义色是深色的正确思路
深色模式不是「反色」,而是「同语义、不同值」:
# 错误示范: 硬编码白底黑字,深色下刺眼
# 正确: 语义 token 切换
# surface: 亮→白 / 暗→深灰(降低亮度对比,不刺眼)
# primary: 亮→深蓝 / 暗→浅蓝(提高明度,暗底可读)
# 原则: 暗色下"大面积用暗色调、强调色提亮",别简单取反
四、字体与动态字型自适应
4.1 文本缩放(可访问性核心)
Flutter 提供 textScaleFactor,系统字体放大时文本自动跟随:
// 系统字体放大 → Flutter 文本自动放大(默认行为)
// 自定义文本样式要确保: 不设置固定 height 导致被裁剪
// 固定容器(高度定死)会在字体放大时溢出 → 用 Flexible/FittedBox
工程要点:
- 别锁
textScaleFactor:禁用缩放会违背无障碍(视障用户靠放大字体阅读)。 - 弹性布局:行高/固定高度在放大下会爆,用
minLines/maxLines+ 弹性容器。 - 中文特性:中文字符密度高,放大阈值内先保证不溢出,再做截断/换行策略。
4.2 动态字体与品牌字体
- 品牌字体:
textTheme里统一挂字体族,不逐个 Text 设置。 - 本地化字体:中文用
fonts+fontVariations或FontWeight匹配,避免全量嵌字体拖体积。 - 等宽字体:数字/代码场景
fontFeatures对齐。
五、响应式与多平台适配
5.1 布局维度的自适应
「写一套,处处跑」需要布局策略:
# 1) 断点: 用 MediaQuery.sizeOf 判断宽度
# <600 手机单栏 / 600-1024 平板双栏 / >1024 桌面多栏
# 2) 自适应组件:
# LayoutBuilder + 约束判断
# Row/Column 换 Wrap/Flexible 弹性布局
# 列表 ↔ 网格: 按宽度动态 gridDelegate
# 3) 输入方式: 桌面键盘事件(Shortcuts/Actions)、
# 鼠标 hover(MouseRegion)、触控版拖拽
// 断点示例
final width = MediaQuery.sizeOf(context).width;
final columns = width >= 1200 ? 4 : width >= 600 ? 2 : 1;
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: columns),
itemBuilder: ...,
)
5.2 安全区与系统 UI 适配
- SafeArea:刘海屏/圆角屏内容不被遮挡。
- 系统栏:状态栏/导航栏颜色用
AnnotatedRegion<SystemUiOverlayStyle>跟随主题。 - 旋转/分屏:
MediaQuery实时响应尺寸变化,布局要「随时可重排」。
六、多品牌与主题扩展
6.1 品牌主题工厂
一个 App 多品牌/多主题(如换肤、多租户),用「品牌 id → ThemeData」工厂:
final themes = {
'default': buildTheme(Brightness.light),
'brand_a': buildBrandTheme('a', Brightness.light),
'brand_b': buildBrandTheme('b', Brightness.light),
};
// 切换: MaterialApp.theme = themes[brandId]
// 扩展: 新增品牌 = 加一个工厂方法,组件代码零改动
6.2 主题的收敛与演进
- Token 单一来源:改品牌 = 改 token 文件,别散改组件。
- 灰度验证:新主题上线前用多尺寸/多系统字体截图对比(golden test)。
- 回归:主题相关改动跑 widget 测试,防止「深色下文字看不清」这种隐性回归。
FAQ
Q:ThemeData 和 Design Token 什么关系?
A:Token 是「设计语言的数据」,ThemeData 是「Flutter 的消费容器」。Token 定义语义与值,ThemeData 把它们装进 Flutter 组件能读的结构。
Q:深色模式要不要做?
A:现在必须做——系统级深色已是默认预期,不做等于主动劝退夜间用户。用语义 token 做,成本反而低。
Q:textScaleFactor 能禁用吗?
A:能但别。它服务视障用户,禁用违背无障碍。正确做法是让布局弹性化,而不是锁死字号。
Q:响应式是不是加几个 MediaQuery 就行?
A:不是。断点 + 弹性布局 + 输入差异 + 安全区,四者齐上才算真自适应。先确定「目标形态」(手机为主还是全平台)。
相关阅读
- Flutter 主题与设计系统 — Theme 机制底层
- Flutter Widgets 布局 — 响应式布局基础
- Flutter 本地化 — 多语言与文本缩放配合
- Flutter Web 与桌面 — 多平台适配实践
- Flutter 可访问性 — 字体缩放的语义基础
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。