Flutter gen_defaults 工具解析:Material 组件默认值如何从 Token 数据库生成
在 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」一节):
- 更新
generated/used_tokens.csv(记录本次生成实际用到的 token); - 更新冻结 SDK 中各 Material 组件的 Theme 文件——即把 packages/flutter/lib/src/material 下各组件文件里
// BEGIN GENERATED TOKEN PROPERTIES与// END GENERATED TOKEN PROPERTIES之间的代码块整体重写。
二、入口脚本:整体执行流程
bin/gen_defaults.dart 的 main 函数清晰地展示了整个生成流程,可归纳为四步。
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.dart、action_chip.dart、choice_chip.dart、filter_chip.dart、input_chip.dart |
AppBarTemplate / BottomAppBarTemplate |
app_bar.dart、bottom_app_bar.dart |
ButtonTemplate(5 次,分别对应 elevated / filled / filled-tonal / outlined / text 按钮) |
elevated_button.dart、filled_button.dart、outlined_button.dart、text_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.dart、time_picker.dart |
MenuTemplate / PopupMenuTemplate |
menu_anchor.dart、popup_menu.dart |
NavigationBarTemplate / NavigationRailTemplate / NavigationDrawerTemplate |
navigation_bar.dart 等 |
SliderTemplate / RangeSliderTemplate |
slider.dart、range_slider.dart |
SwitchTemplate / CheckboxTemplate / RadioTemplate |
switch.dart、checkbox.dart、radio.dart |
TextFieldTemplate / InputDecoratorTemplate |
text_field.dart、input_decorator.dart |
TypographyTemplate / MotionTemplate |
typography.dart、motion.dart |
SurfaceTintTemplate |
elevation_overlay.dart |
…其余包括 Badge、Banner、BottomSheet、Divider、Drawer、ExpansionTile、IconButton、ListTile、ProgressIndicator、SearchBar / SearchView、SegmentedButton、Snackbar、Tabs 等 |
对应组件文件 |
每个模板的构造参数为「块名、目标文件路径、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_CORNERS→RoundedRectangleBorder,且做了三级简写:四角相同且为 0 时省略borderRadius;上下左右成对相同时用BorderRadius.vertical;否则展开为BorderRadius.only四个角;SHAPE_FAMILY_CIRCULAR→StadiumBorder();- 其他族会打印
Unsupported shape family type并返回空串。
updateFile():块级幂等替换
updateFile()(template.dart 第 82–112 行)是整个工具「可以反复运行、只改生成块、不动手写代码」的关键:
- 读取目标文件全文,查找标记
// BEGIN GENERATED TOKEN PROPERTIES - <blockName>与对应的// END GENERATED TOKEN PROPERTIES - <blockName>; - 若找到且块结构合法(end 在 begin 之后),记录块前后的手写内容,随后用新生成的
generate()输出替换旧块;若找不到标记,则把新块追加到文件末尾;若 begin 存在但 end 缺失/乱序,打印Unable to find block named ...并跳过,不做破坏性写入; - 新块由四部分组成: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 1 与 Block 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.0,md.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.json、fab_extended_primary.json、button_elevated.json、chip_assist.json、dialog.json、menu.json、slider.json、switch.json、checkbox.json、search_bar.json、snackbar.json、progress_indicator.json、navigation_bar.json等,每个文件对应一类组件的一组md.comp.*token; - 系统/基础 token:
color_light.json、color_dark.json(亮/暗系统颜色,单独加载)、elevation.json(md.sys.elevation.*层级表)、shape.json(md.sys.shape.*形状族定义)、motion.json(动效曲线与时长)、typeface.json/text_style.json(字号字重)、palette.json(md.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 定义了一个全局单例 tokenLogger(final TokenLogger tokenLogger = TokenLogger()),职责与 README 所述一致——验证 token 并追踪使用:
init(allTokens:, versionMap:):在入口脚本加载完数据后注入全量 token 与版本映射;log(token):模板每次经getToken(及opacity、elevation等内部调用)取用 token 时都会调用;token 存在则加入排序集合_usedTokens(SplayTreeSet),不存在则加入_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 检测(引用 baz、foobar 两个不存在的 token 时会输出红色错误与名单)、color() 带默认值时不记缺失/不带默认值时记缺失、border() 在 width/height 不同可用性下的记录规则,以及 dumpToFile 的内容断言。这组测试与 dart_test.yaml 一起保证了日志器与 CSV 导出逻辑的稳定性。
七、验证与回归:如何确认生成结果正确
对这套工具的回归验证由 dev/tools/gen_defaults/test/gen_defaults_test.dart 承担,重点覆盖三块能力:
- 块管理行为:向临时文件追加块、重复运行时原位替换块、同文件多块互不干扰——这保证了脚本可以幂等重复执行;
- 形状解析:四角各异时生成
BorderRadius.only,SHAPE_FAMILY_CIRCULAR生成const StadiumBorder(); - 日志与导出:如上节所列,覆盖版本统计、缺失 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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00