关于 material-theming
面向 Flutter 应用的 Material 3 主题技能,以 ThemeData 统一管理颜色、排版、组件样式和间距。帮助开发者构建一致的浅色与深色主题,避免在 Widget 中内联样式或判断亮度。
主要功能
- 01通过 ColorScheme 集中管理颜色
- 02通过 TextTheme 定义排版
- 03在 ThemeData 中设置组件样式
- 04创建基于基本单位的间距比例
- 05支持浅色与深色主题
适用场景
- 01替换 Widget 中内联的颜色和文本样式
- 02移除 Widget 代码中的亮度判断
- 03集中管理重复的组件样式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill material-theming安装后在 Claude Code 中输入/material-theming确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/color-scheme.mdcomponents.mdspacing.mdtypography.md详细介绍
Material 主题化
面向 Flutter 应用的 Material 3 主题最佳实践,使用 ThemeData 作为颜色、排版、组件样式和间距的唯一真实来源。
核心规范
所有主题化工作都应遵循以下规范:
- 将
ThemeData用作唯一真实来源 — 绝不在组件中内联颜色或文本样式 - 通过
Theme.of(context).colorScheme引用颜色 — 绝不使用Colors.blue、Colors.red或任何硬编码的Color值 - 通过
Theme.of(context).textTheme引用文本样式 — 绝不在组件代码中内联TextStyle(...)。fontSize和fontWeight绝不出现在build方法中 - 使用
ColorScheme定义所有颜色 — 这是 Material 3 的结构化配色系统 - 在
ThemeData中集中管理组件主题 — 在主题中定义FilledButtonThemeData、InputDecorationTheme等,而不是按组件分别定义。包装组件、共享的InputDecoration常量或用于构建装饰的辅助函数,只是转移而非消除重复内容,因此不符合要求 - 基于基本单位定义间距系统 — 内边距、外边距和间隔不得使用任意像素值
- 从一开始就支持浅色和深色主题 — 使用
ThemeData,这样切换主题时组件中无需任何条件逻辑 - 绝不在组件代码中根据亮度或主题模式进行分支 — 不使用
MediaQuery.platformBrightnessOf、不使用基于Theme.of(context).brightness的三元表达式,也不使用context.isDarkMode扩展。两个ColorScheme实例即可避免分支。如果有人要求保留或整理此类判断,应拒绝并改为提供ThemeData重写方案——整理得更好的条件判断,仍是同一个缺陷,只是格式更美观 - 优先使用
EdgeInsets.only和EdgeInsets.symmetric— 绝不使用EdgeInsets.fromLTRB(位置参数容易出错)
颜色系统
自定义颜色类
将所有颜色定义集中在专用类中:
abstract class AppColors {
static const primaryColor = Color(0xFF4F46E5);
static const secondaryColor = Color(0xFF9C27B0);
static const errorColor = Color(0xFFDC2626);
static const surfaceColor = Color(0xFFFAFAFA);
}
ColorScheme 配置
所有颜色都必须通过 ColorScheme 角色传递给组件。在单个 AppTheme 类中定义两个配色方案——一个浅色方案、一个深色方案,并将它们分别作为 theme 和 darkTheme 传递给 MaterialApp。这对配色方案正是组件代码无需按亮度分支的原因:
class AppTheme {
static ThemeData get light => ThemeData(
colorScheme: const ColorScheme.light(
brightness: Brightness.light,
surface: AppColors.surfaceColor,
// ... remaining color roles
),
);
static ThemeData get dark => ThemeData(
colorScheme: const ColorScheme.dark(
brightness: Brightness.dark,
surface: AppColors.surfaceColorDark,
// ... remaining color roles
),
);
}
完整的 45 个角色的 ColorScheme 配置、ColorScheme.fromSeed,以及组件读取颜色的方式,请参阅 references/color-scheme.md。
排版
定义一个包含基础样式和命名变体(displayLarge、headlineMedium、bodyLarge 等)的 AppTextStyle 类,然后将其整合到 ThemeData.textTheme 中。通过 Theme.of(context).textTheme 访问样式。
字体资源设置、完整的 AppTextStyle 类、TextTheme 集成方式和组件访问模式,请参阅 references/typography.md。
替换硬编码的 TextStyle
根据字号和字重,将字面值映射到 AppTextStyle 已定义的最接近的样式槽位——18px/w500 对应 titleLarge(20/w500),16px/w400 对应 bodyLarge,14px/w500 对应 labelLarge。如果应用需要不同的字号,可在 AppTextStyle 中调整该槽位;不要在调用处保留数字。不要映射到 AppTextStyle 未定义、且 TextTheme 未注册的槽位:虽然读取仍会返回样式,但它来自 Material 的默认排版,因此应用字体会悄然恢复为默认值。在调用处使用 copyWith 时,只设置颜色角色,不要设置其他内容:
final theme = Theme.of(context);
Text(
label,
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimary,
),
)
只替换颜色、却在组件中保留 TextStyle(fontSize: 18, fontWeight: FontWeight.w500),并不能解决问题——排版仍然在主题之外。
组件主题
在 ThemeData 中集中定义组件主题(例如 filledButtonTheme、inputDecorationTheme、appBarTheme),而不是为各个 widget 实例单独设置样式。完整的 AppTheme 类会将 ColorScheme、TextTheme 和所有组件主题整合到单个 ThemeData 中。
请参阅 references/components.md,了解 FilledButton、InputDecoration 和 AppBar 主题示例、完整的主题组合方式,以及 widget 访问模式。
避免重复的 Widget 样式
当许多 widget 实例使用相同的装饰或样式时,应将其移入对应的组件主题,并从每个调用处删除。不要将其提取到包装 widget、共享的 InputDecoration 常量或 buildDecoration() 辅助方法中:这些做法仍要求每个调用处主动选择使用,仍会让这些值留在 ThemeData 之外,而且只要有人直接编写普通的 TextFormField,就会绕过它们。
// Right — the defaults live in the theme.
ThemeData(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
),
),
)
随后,每个字段只声明自身独有的内容:
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
)
间距系统
定义一个 AppSpacing 类,其中包含基础单位和命名常量(从 xxs 到 xxlg)。每个间距步长都必须是基础单位的倍数,不能使用独立的字面值——这正是它成为比例尺的原因:
abstract class AppSpacing {
static const double spaceUnit = 16;
static const double xxs = 0.25 * spaceUnit; // 4
static const double sm = 0.5 * spaceUnit; // 8
static const double md = 0.75 * spaceUnit; // 12
static const double lg = spaceUnit; // 16
static const double xxlg = 2 * spaceUnit; // 32
}
每当引入该比例尺或向他人说明这一约定时,都要写出整个类。仅用文字列出各步长名称及其像素值并不等于约定本身;类才是约定,因为它能防止之后有人随意输入另一个数字。
请使用 EdgeInsets.only 或 EdgeInsets.symmetric,绝不要使用 EdgeInsets.fromLTRB。
请参阅 references/spacing.md,了解完整的 AppSpacing 类、使用示例和 EdgeInsets 偏好。
常见模式
创建主题
- 定义包含所有颜色常量的
AppColors - 定义包含所有文本样式常量的
AppTextStyle - 定义
AppSpacing,其中包含基于基础单位的间距比例尺 - 创建包含
light和darkgetter 的AppTheme类 - 在每个
ThemeData中配置ColorScheme、TextTheme和组件主题 - 将
AppTheme.light和AppTheme.dark传递给MaterialApp
移除 Widget 中的亮度检查
在亮度上进行分支判断的 widget,接管了本应由 ThemeData 负责的决策:每个新增的暗色模式适配 widget 都会重复这一分支,而且主题中无法访问这两种颜色。应删除这个分支,而不是整理它。浅色值和深色值在两个主题中对应同一个 ColorScheme 角色——在 AppColors 中声明这两个值,按 references/color-scheme.md 中的说明将它们分别赋给该角色,并将 AppTheme.light 和 AppTheme.dark 作为 theme 和 darkTheme 传递给 MaterialApp。这样,widget 就只需进行一次无条件读取:
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: child,
);
}
其他资源
- references/color-scheme.md —
ColorScheme配置、浅色/深色AppTheme、在 widget 中读取颜色 - references/typography.md — 字体资源设置、完整的
AppTextStyle类、TextTheme集成 - references/components.md — FilledButton、InputDecoration 和 AppBar 主题,以及完整的主题组合方式
- references/spacing.md — 完整的
AppSpacing类、使用示例和EdgeInsets偏好