mcpField
M
material-theming

material-theming

github.com/verygoodopensource设计与 UI175129 次安装今天更新
代码仓库

关于 material-theming

面向 Flutter 应用的 Material 3 主题技能,以 ThemeData 统一管理颜色、排版、组件样式和间距。帮助开发者构建一致的浅色与深色主题,避免在 Widget 中内联样式或判断亮度。

主要功能

  1. 01通过 ColorScheme 集中管理颜色
  2. 02通过 TextTheme 定义排版
  3. 03在 ThemeData 中设置组件样式
  4. 04创建基于基本单位的间距比例
  5. 05支持浅色与深色主题

适用场景

  1. 01替换 Widget 中内联的颜色和文本样式
  2. 02移除 Widget 代码中的亮度判断
  3. 03集中管理重复的组件样式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill material-theming

安装后在 Claude Code 中输入/material-theming确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
references/
color-scheme.md
components.md
spacing.md
typography.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 偏好。

常见模式

创建主题
  1. 定义包含所有颜色常量的 AppColors
  2. 定义包含所有文本样式常量的 AppTextStyle
  3. 定义 AppSpacing,其中包含基于基础单位的间距比例尺
  4. 创建包含 light 和 dark getter 的 AppTheme 类
  5. 在每个 ThemeData 中配置 ColorScheme、TextTheme 和组件主题
  6. 将 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,
  );
}

其他资源

评论

支持 Markdown