
最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。
接下来的几题,将从一个需求逐步演进,让答题者给出方案设计。上一题已经小试牛刀,完成了所有 StatelessWdiget 组件名称的提取。本题将更进一步,焦点是探讨:
收集 SDK 所有 Widget 组件基本信息
请你设计一套方案,通过 dart 的命令行脚本,分析并提取 Flutter SDK 中 所有的 Widget 的派生类信息。每个组件的信息如下,包括名称、所在路径、超类列表、组件描述信息。

1. 组件数据
组件数据基于题目中的 json 数据,可以定义如下的 WidgetPo
类承载解析后的数据。
dart
class WidgetPo {
final String name;
final String path;
final bool isAbstract;
final String? desc;
WidgetPo({
required this.name,
required this.path,
required this.parents,
required this.desc,
});
Map<String, dynamic> toJson() {
return {
'name': name,
'path': path,
'parents': parents,
'desc': desc,
};
}
}
2. 解析器 WidgetVisitor
上一题的这个流程就不赘述了,核心在于通过继承 RecursiveAstVisitor
实现解析器。其中可以校验一下当父类列表中包含 Widget
,则表示该类型继承自 Widget
, 这样就可以轻而易举地得到所有 Widget 的派生类。是不是非常简单。
如果按上一题,将名称打印出来,我们就可以得到 Flutter 源码中的所有组件名称。目前 Flutter 3.27.4
中非下划线的组件一共 555 个。

dart
class WidgetVisitor extends RecursiveAstVisitor<void> {
final String filePath;
WidgetVisitor(this.filePath);
final List<WidgetPo> components = [];
@override
void visitClassDeclaration(ClassDeclaration node) {
List<InterfaceType>? types = node.declaredFragment?.element.allSupertypes;
String target = 'Widget';
bool? hit = types?.where((e) => e.element3.name3 == target).isNotEmpty;
if (hit ?? false) {
components.add(parserWidgetClass(node, types));
}
super.visitClassDeclaration(node);
}
WidgetPo parserWidgetClass(ClassDeclaration node,List<InterfaceType>? types){
/// TODO 解析组件类信息
}
}
上面的 WidgetVisitor
中维护了 List<WidgetPo>
列表,并定义了 parserWidgetClass
方法解析必要的字段,创建 WidgetPo 对象。然后在类是 Widget 生类时,触发 parserWidgetClass 添加到结果列表里。
3. 解析类信息并输出
最后,最需要的就是完成 parserWidgetClass 方法:
- 这里超类只统计到
Widget
为止,否则会有很多冗余数据。毕竟 Widget 之上的父类都是一样的。 - 通过
documentationComment
方法可以得到注释信息,每一行是一个Token
对象。一般来说,组件源码的第一句,都会简单扼要地介绍这个组件,所有用它作为描述信息非常适合。

dart
WidgetPo parserWidgetClass(ClassDeclaration node,List<InterfaceType>? types){
final String className = node.name.lexeme;
List<String> parents = [];
for (InterfaceType type in types ?? []) {
String? name = type.element3.name3;
if (name != null) {
parents.add(name);
}
if (name == 'Widget') {
break;
}
}
List<Token> tokens = node.documentationComment?.tokens??[];
String desc = '';
for(Token token in tokens){
String value = token.lexeme;
if(value=='///'){
break;
}
desc+=value.replaceAll('///', '');
}
return WidgetPo(
name: className,
path: filePath,
desc: desc,
parents: parents,
);
}
4. 成果与输出
最后将所有解析的 WidgetPo 列表按照名称排序,然后通过 json 编码成字符串,就可以得到 史上最全 组件信息数据。当你自己跑出来这些这些数据时,或许也会和我一样非常兴奋和开心。有了整理分析的手段,借助这些数据,我们就可以做出很多有趣的统计、展示。这也是后续的挑战 ~
dart
components.sort((a, b) => a.name.compareTo(b.name));
File outfile = File('Widget.json');
await outfile.writeAsString(jsonEncode(components));
print('Results saved to: ${outfile.path}');

5. 小结
本题我们继续接触 基于 ClassDeclaration 统计信息 ,完成了所有组件列表基本数据的抓取工作。《匠心星问》系列题库设计的初衷:不仅是"会不会用",而是鼓励开发者去思考 "怎么做才更合理、更系统" 。希望你在解决本题的过程中,收获的不只是 Flutter 技术本身,更是一种解决问题的方式。
后续的《每日一题》也将持续围绕实战问题展开,欢迎持续关注和参与,一起构建一个 开放、精致、有深度 的 Flutter 题库生态!
你是否也有想出的题目,欢迎投稿加入《匠心星问》共同打造!
如果你有其他的看法,或者有什么想要的题目、或者想提供题目和答案,都欢迎在评论区留言。更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。