首页
/ Flutter gen_defaults 工具解析:Material 组件默认值如何从 Token 数据库生成

Flutter gen_defaults 工具解析:Material 组件默认值如何从 Token 数据库生成

2026-09-06 15:41:48作者:明树来

在 Flutter 的 Material 组件库中,按钮、FAB、卡片等组件的默认颜色、圆角、字号与尺寸并非手工硬编码,而是由 dev/tools/gen_defaults 这个代码生成工具从 Material Design Token 数据库自动注入。本文基于 dev/tools/gen_defaults/README.md 及其配套源码,完整讲清该工具的运行方式、模板机制(TokenTemplate 体系)、Token 数据组织与使用日志(used_tokens.csv)的生成原理,帮助开发者理解 Material 默认值的来源、更新流程,以及如何在冻结版 SDK 组件中定位生成代码块。

一、工具定位与使用前提

README 首先给出了一条重要警示:该工具已被标记为 legacy(遗留工具)并已弃用(deprecated)。它只针对冻结的 SDK 内置 Material 库,即 packages/flutter/lib/src/material 下的组件,不应再用于新的 Material 默认值工作。活跃的 Material 默认值生成已迁移到 flutter/packages 仓库中的独立 material_ui 包下的 material_ui/tool/gen_defaults 工具,新的工作应去那边进行。

pubspec.yaml 可以看到该工具的依赖声明:

name: gen_defaults
description: A command line script to generate Material component defaults from the token database.
version: 1.0.0

environment:
  sdk: ^3.11.0-0

resolution: workspace

dependencies:
  args: any

即它依赖 args 包做命令行参数解析,属于 Flutter 根仓库 Dart workspace 的一部分,因此必须从 git 仓库根目录运行

运行命令

从仓库根目录执行(README 原文命令,入口脚本 头部注释与此一致):

dart dev/tools/gen_defaults/bin/gen_defaults.dart [-v]

参数只有一个:-v / --verbose,用于打印详细的 token 版本与使用情况(入口脚本第 70–73 行通过 ArgParser 定义)。

运行后的效果是(对应 README「Legacy Usage」一节):

  1. 更新 generated/used_tokens.csv(记录本次生成实际用到的 token);
  2. 更新冻结 SDK 中各 Material 组件的 Theme 文件——即把 packages/flutter/lib/src/material 下各组件文件里 // BEGIN GENERATED TOKEN PROPERTIES// END GENERATED TOKEN PROPERTIES 之间的代码块整体重写。

二、入口脚本:整体执行流程

bin/gen_defaults.dartmain 函数清晰地展示了整个生成流程,可归纳为四步。

1. 参数解析与数据加载

脚本定义了两个关键路径常量:

const String materialLib = 'packages/flutter/lib/src/material';
const String dataDir = 'dev/tools/gen_defaults/data';

随后遍历 dev/tools/gen_defaults/data/ 目录下的全部 JSON token 文件Directory(dataDir).listSync()),对每个文件:

  • 读取 version 字段(如 6_1_0),构建「版本号 → 文件名列表」的 versionMap
  • 移除 version 键后,把该文件的其余键值对合并进全局 tokens 映射表。

也就是说,所有 JSON 文件最终被合并成一张扁平的 token 名称 → 值 大字典,供后续所有模板使用。

2. 亮色/暗色颜色 token 单独处理

亮色与暗色的系统颜色 token(md.sys.color.*)名称完全相同,无法合并到同一字典,因此被单独读取(入口脚本第 91–93 行):

final Map<String, dynamic> colorLightTokens = _readTokenFile(File('$dataDir/color_light.json'));
final Map<String, dynamic> colorDarkTokens = _readTokenFile(File('$dataDir/color_dark.json'));

color_light.json 为例,其内容形如:

{
  "version": "6_1_0",
  "md.sys.color.background": "md.ref.palette.neutral98",
  "md.sys.color.error": "md.ref.palette.error40",
  "md.sys.color.primary": "md.ref.palette.primary40",
  "md.sys.color.primary-container": "md.ref.palette.primary90",
  ...
}

可见系统级颜色 token 的值仍然是对调色板 token(md.ref.palette.*)的引用,属于 Material token 数据库的「引用链」结构。

3. 实例化约 40 个组件模板并逐个更新文件

入口脚本的中段(第 96–151 行,特意用 dart format off 包裹以保持可读性)是所有模板的集中调用点,覆盖冻结 SDK 的全部主要 Material 组件:

模板类 目标组件 / 文件
ChipTemplate / ActionChipTemplate / FilterChipTemplate / InputChipTemplate chip.dartaction_chip.dartchoice_chip.dartfilter_chip.dartinput_chip.dart
AppBarTemplate / BottomAppBarTemplate app_bar.dartbottom_app_bar.dart
ButtonTemplate(5 次,分别对应 elevated / filled / filled-tonal / outlined / text 按钮) elevated_button.dartfilled_button.dartoutlined_button.darttext_button.dart
CardTemplate(3 次:elevated / filled / outlined card) card.dart
FABTemplate floating_action_button.dart
ColorSchemeTemplate(接收亮/暗两套颜色 token) theme_data.dart
DialogTemplate / DialogFullscreenTemplate dialog.dart
DatePickerTemplate / TimePickerTemplate date_picker_theme.darttime_picker.dart
MenuTemplate / PopupMenuTemplate menu_anchor.dartpopup_menu.dart
NavigationBarTemplate / NavigationRailTemplate / NavigationDrawerTemplate navigation_bar.dart
SliderTemplate / RangeSliderTemplate slider.dartrange_slider.dart
SwitchTemplate / CheckboxTemplate / RadioTemplate switch.dartcheckbox.dartradio.dart
TextFieldTemplate / InputDecoratorTemplate text_field.dartinput_decorator.dart
TypographyTemplate / MotionTemplate typography.dartmotion.dart
SurfaceTintTemplate elevation_overlay.dart
…其余包括 BadgeBannerBottomSheetDividerDrawerExpansionTileIconButtonListTileProgressIndicatorSearchBar / SearchViewSegmentedButtonSnackbarTabs 对应组件文件

每个模板的构造参数为「块名、目标文件路径、token 字典」,随后调用 updateFile() 完成写入。例如按钮类通过 token 前缀区分变体:

ButtonTemplate('md.comp.elevated-button', 'ElevatedButton', '$materialLib/elevated_button.dart', tokens).updateFile();
ButtonTemplate('md.comp.filled-button', 'FilledButton', '$materialLib/filled_button.dart', tokens).updateFile();

4. 打印使用情况并导出 CSV

所有模板执行完毕后(入口脚本第 153–159 行):

tokenLogger.printVersionUsage(verbose: verbose);
tokenLogger.printTokensUsage(verbose: verbose);
if (!verbose) {
  print('\nTo see detailed version and token usage, run with --verbose (-v).');
}
tokenLogger.dumpToFile('dev/tools/gen_defaults/generated/used_tokens.csv');

即无论是否 verbose,都会向 dev/tools/gen_defaults/generated/used_tokens.csv 落盘。仓库中现存的 used_tokens.csv 共 1041 行,首行为 Versions used, 6_1_0,其后是按字母排序的已使用 token 清单(如 md.comp.assist-chip.container.shape),这正是 README 所说「脚本验证 token 并追踪哪些 token 被使用」的产物,可用于审计 token 数据库的覆盖率。

三、模板体系:TokenTemplate 基类

README 指出:每个需要默认值的组件对应一个模板文件,模板都是 TokenTemplate 的子类;基类提供了工具函数,并定义了「把一段生成代码追加到目标文件底部」的结构;子类必须重写 generate() 方法,以字符串形式返回要生成的代码块。

lib/template.dart 完整实现了这一机制,核心成员如下。

构造参数与可定制前缀

abstract class TokenTemplate {
  const TokenTemplate(
    this.blockName,
    this.fileName,
    this._tokens, {
    this.colorSchemePrefix = 'Theme.of(context).colorScheme.',
    this.textThemePrefix = 'Theme.of(context).textTheme.',
  });
  • blockName:生成代码块的标识名,用于在目标文件中定位已有块以便替换;
  • fileName:被更新的目标文件(即 packages/flutter/lib/src/material/xxx.dart);
  • colorSchemePrefix / textThemePrefix:颜色与文字样式 token 解析结果前拼接的前缀,子类可按需改写。

一个典型的改写示例见 fab_template.dart:FAB 默认值类内部使用局部字段 _colors_textTheme,因此构造时传入 super.colorSchemePrefix = '_colors.'super.textThemePrefix = '_textTheme.',使生成的 getter 直接引用局部缓存而非重复调用 Theme.of(context)

核心 API

方法 作用 说明
tokenAvailable(String) 判断 token 是否存在 _tokens.containsKey
getToken(String, {optional}) 读取原始 token 值并记录使用日志 optional: true 且 token 缺失时返回 null 且不记为「缺失引用」;optional: false 且缺失时会被 TokenLogger 记入 _unavailableTokens,结束时以红色警告打印
updateFile() 替换或追加生成代码块 见下节
generate() 抽象方法,由子类实现 返回生成代码字符串
color(token, [defaultValue]) 解析颜色 token 存在则返回 colorSchemePrefix + token值;否则返回 defaultValue ?? 'null'
colorOrTransparent(token) 颜色或透明色 缺省时回退为 Colors.transparent
componentColor(componentToken) 组件颜色(支持 opacity) 若存在 <token>.opacity 则追加 .withOpacity(x),如 _colors.onPrimaryContainer.withOpacity(0.1)
opacity(token) 解析不透明度 数值转字符串
elevation(componentToken) 解析 elevation 注意是两级查表getToken('$token.elevation') 取出的是一个 token 名(如 md.sys.elevation.level6),再查一次得到数值
size(componentToken) 解析尺寸 .size 生成 const Size.square(x);否则由 .width/.height 生成 const Size(w, h),两者皆缺则抛异常
shape(componentToken, [prefix]) 解析形状 见下
border(componentToken) 生成 BorderSide 无颜色返回 null;宽度取 .width(回退 .height,再回退 1.0),宽度为 1.0 时省略 width: 参数
textStyle(componentToken) / textStyleWithColor 解析文字样式 返回 textThemePrefix + token值,后者若存在颜色 token 还会追加 ?.copyWith(color: ...)

shape() 的形状族映射规则(template.dart 第 226–258 行):

  • SHAPE_FAMILY_ROUNDED_CORNERSRoundedRectangleBorder,且做了三级简写:四角相同且为 0 时省略 borderRadius;上下左右成对相同时用 BorderRadius.vertical;否则展开为 BorderRadius.only 四个角;
  • SHAPE_FAMILY_CIRCULARStadiumBorder()
  • 其他族会打印 Unsupported shape family type 并返回空串。

updateFile():块级幂等替换

updateFile()(template.dart 第 82–112 行)是整个工具「可以反复运行、只改生成块、不动手写代码」的关键:

  1. 读取目标文件全文,查找标记 // BEGIN GENERATED TOKEN PROPERTIES - <blockName> 与对应的 // END GENERATED TOKEN PROPERTIES - <blockName>
  2. 若找到且块结构合法(end 在 begin 之后),记录块前后的手写内容,随后用新生成的 generate() 输出替换旧块;若找不到标记,则把新块追加到文件末尾;若 begin 存在但 end 缺失/乱序,打印 Unable to find block named ... 并跳过,不做破坏性写入;
  3. 新块由四部分组成:begin 标记 → 固定头部注释(说明本块由 dev/tools/gen_defaults/bin/gen_defaults.dart 从 Material Design token 数据库生成,并带 dart format off)→ generate() 输出 → dart format on 尾部 → end 标记。头部注释中的 dart format off/on 用于保证生成代码不被格式化器重排(源码中留有 TODO,计划未来输出自动格式化后的代码并去掉这对开关)。

测试文件 test/gen_defaults_test.dart 用三个用例精确锁定了这一行为:向空文件追加、对已有块原位替换、同一文件内多个命名块互不干扰Block 1Block 2 分别更新时互不破坏)。此外还有 shape() 的四角圆角与全圆角(StadiumBorder)单测。

四、模板示例:FAB 的默认值生成

lib/fab_template.dart 是 README 推荐的阅读示例。其 generate() 返回一个 _FABDefaultsM3 extends FloatingActionButtonThemeData 类的完整文本,全部数值来自 token:

class FABTemplate extends TokenTemplate {
  const FABTemplate(
    super.blockName,
    super.fileName,
    super.tokens, {
    super.colorSchemePrefix = '_colors.',
    super.textThemePrefix = '_textTheme.',
  });

  @override
  String generate() =>
      '''
class _${blockName}DefaultsM3 extends FloatingActionButtonThemeData {
  _${blockName}DefaultsM3(this.context, this.type, this.hasChild)
    : super(
        elevation: ${elevation("md.comp.fab.primary.container")},
        focusElevation: ${elevation("md.comp.fab.primary.focus.container")},
        ...
        sizeConstraints: const BoxConstraints.tightFor(
          width: ${getToken("md.comp.fab.primary.container.width")},
          height: ${getToken("md.comp.fab.primary.container.height")},
        ),
        ...
      );
  ...
  @override Color? get foregroundColor => ${componentColor("md.comp.fab.primary.icon")};
  @override Color? get backgroundColor => ${componentColor("md.comp.fab.primary.container")};
  @override Color? get splashColor => ${componentColor("md.comp.fab.primary.pressed.state-layer")};
  ...
}
''';
}

运行工具后,packages/flutter/lib/src/material/floating_action_button.dart 中的生成块即为模板的落地结果,例如:

// BEGIN GENERATED TOKEN PROPERTIES - FAB

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

// dart format off
class _FABDefaultsM3 extends FloatingActionButtonThemeData {
  _FABDefaultsM3(this.context, this.type, this.hasChild)
    : super(
        elevation: 6.0,
        focusElevation: 6.0,
        hoverElevation: 8.0,
        ...
        sizeConstraints: const BoxConstraints.tightFor(width: 56.0, height: 56.0),
        ...
      );
  ...
  @override Color? get foregroundColor => _colors.onPrimaryContainer;
  @override Color? get splashColor => _colors.onPrimaryContainer.withOpacity(0.1);
  ...
// END GENERATED TOKEN PROPERTIES - FAB

可以看到 md.comp.fab.primary.container.height 之类的 token 最终变成了 56.0md.comp.fab.primary.container.shape 变成了 RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),而带透明度的状态层颜色被解析成 _colors.onPrimaryContainer.withOpacity(0.1) 这样的 Dart 表达式。material 目录下其余组件文件中同样能找到各自的 BEGIN GENERATED TOKEN PROPERTIES - <块名> 标记块。

五、Token 数据:data/ 目录的 JSON 文件

README 说明 token 以 JSON 文件形式存放在 data/ 目录,数据源是 Google 内部数据库。当前仓库 dev/tools/gen_defaults/data/ 下有 58 个 JSON 文件,覆盖组件 token 与系统 token 两大类:

  • 组件 token:如 fab_primary.jsonfab_extended_primary.jsonbutton_elevated.jsonchip_assist.jsondialog.jsonmenu.jsonslider.jsonswitch.jsoncheckbox.jsonsearch_bar.jsonsnackbar.jsonprogress_indicator.jsonnavigation_bar.json 等,每个文件对应一类组件的一组 md.comp.* token;
  • 系统/基础 tokencolor_light.jsoncolor_dark.json(亮/暗系统颜色,单独加载)、elevation.jsonmd.sys.elevation.* 层级表)、shape.jsonmd.sys.shape.* 形状族定义)、motion.json(动效曲线与时长)、typeface.json / text_style.json(字号字重)、palette.jsonmd.ref.palette.* 调色板,被上层 token 引用)、state.json(交互状态)。

每个文件都有 version 字段(如 6_1_0),入口脚本据此汇总出 token 数据库的版本使用情况;由于所有文件合并为同一字典,token 之间可以通过「值是另一个 token 名」的方式形成引用链(例如系统颜色指向调色板条目、组件 elevation 指向 md.sys.elevation.level*),这也是 elevation() 需要两级查表的原因。

六、Token 使用日志:TokenLogger 与 used_tokens.csv

lib/token_logger.dart 定义了一个全局单例 tokenLoggerfinal TokenLogger tokenLogger = TokenLogger()),职责与 README 所述一致——验证 token 并追踪使用

  • init(allTokens:, versionMap:):在入口脚本加载完数据后注入全量 token 与版本映射;
  • log(token):模板每次经 getToken(及 opacityelevation 等内部调用)取用 token 时都会调用;token 存在则加入排序集合 _usedTokensSplayTreeSet),不存在则加入 _unavailableTokens
  • printVersionUsage({verbose}):打印 Versions used: ...,verbose 时按版本逐条列出数据文件名;
  • printTokensUsage({verbose}):打印 Tokens used: 已用/总数;verbose 时逐条用 / 标记;若存在缺失引用,用红色 ANSI 转义(\x1B[31m...)打印 Some referenced tokens do not exist: N 及具体名单——这是发现「模板引用了数据库中不存在的 token」的第一道防线;
  • dumpToFile(path):把版本行与全部已用 token 写入 CSV。

测试文件对日志器有一组完整的行为验证(Tokens logger group):空状态打印、版本打印的普通/verbose 两种形态、正常计数(Tokens used: 1/1)、无效 token 检测(引用 bazfoobar 两个不存在的 token 时会输出红色错误与名单)、color() 带默认值时不记缺失/不带默认值时记缺失、border()width/height 不同可用性下的记录规则,以及 dumpToFile 的内容断言。这组测试与 dart_test.yaml 一起保证了日志器与 CSV 导出逻辑的稳定性。

七、验证与回归:如何确认生成结果正确

对这套工具的回归验证由 dev/tools/gen_defaults/test/gen_defaults_test.dart 承担,重点覆盖三块能力:

  1. 块管理行为:向临时文件追加块、重复运行时原位替换块、同文件多块互不干扰——这保证了脚本可以幂等重复执行;
  2. 形状解析:四角各异时生成 BorderRadius.onlySHAPE_FAMILY_CIRCULAR 生成 const StadiumBorder()
  3. 日志与导出:如上节所列,覆盖版本统计、缺失 token 报警与 CSV dump。

除单测外,日常验证手段是:运行脚本后用 git diff 检查 packages/flutter/lib/src/material/ 下各组件文件的生成块是否与预期一致(块内代码有 dart format off 保护,diff 只应来自 token 值变化),并检查 generated/used_tokens.csv 的 token 清单与版本头行(当前为 Versions used, 6_1_0)。

八、小结与适用范围

  • 该工具是面向冻结 SDK Material 库的遗留生成管线:读取 data/ 下的 Material token JSON → 合并为全局 token 字典 → 约 40 个 TokenTemplate 子类按 md.comp.* / md.sys.* token 生成各组件的 *DefaultsM3 代码块 → 以命名标记块方式幂等写回 packages/flutter/lib/src/material/*.dart → 输出 token 使用统计并落盘 used_tokens.csv
  • 新组件或新默认值工作应转向 flutter/packages 仓库中 material_ui 包的 material_ui/tool/gen_defaults 工具,本文工具不再作为新的工作入口,但其模板机制(TokenTemplate + 标记块替换 + token 日志审计)仍是理解现有 Material 默认值代码来源的关键。
  • 适用前提:必须使用 Dart SDK ^3.11.0-0 环境,并从本仓库根目录以 dart dev/tools/gen_defaults/bin/gen_defaults.dart [-v] 方式运行;由于脚本会改写 material 库源文件,建议在可丢弃的工作区副本中运行以对比 diff。
登录后查看全文
热门项目推荐
相关项目推荐