Flutter 百题斩#15 | 列出 SDK 所有 StatelesWidget 组件

108 阅读2分钟


最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。
加下来的几题,将从一个需求逐步演进,让答题者给出方案设计。本题的焦点是探讨:

列出 Flutter SDK 中所有 StatelesWidget 组件


1. 题目概述

请你设计一套方案,通过 dart 的命令行脚本,分析并提取 Flutter SDK 中所有的 StatelesWidget 的派生类。基础代码如下所示,命令行中传入 flutter_sdk 路径。题目考查:

  • 对 Flutter SDK 文件目录的了解
  • 对文件操作接口熟悉度
  • 解析 Dart 类和整理数据的能力
void main(List<String> args) async {
  int length = args.length;
  if (length < 1) {
    print('run with <flutter_sdk_path>');
    return;
  }

  final flutterSdkPath = args[0];
  print('Parsing Flutter SDK at: $flutterSdkPath');
  await parser(flutterSdkPath);
}

Future<void> parser(String sdkPath) async{
  /// TODO 解析 sdk 中所有的 StatelessWidget 派生类
  /// 得到组件名列表
}

2. 思路分析

实现,我们应该知道,Flutter SDK 中的组件都在 packages/flutter/lib/src 下。随便点击一个组件,进入源码,悬浮时就可以看到路径:

可以进入对应的文件夹中参观一下,如下所示。接下来就需要遍历其中的文件,收集期望的数据。


文件有了,但是如何解析 dart 文件是比较头疼的。如果单纯自己使用正则匹配,虽然可行,但是操作太复杂。Flutter 官方有一个 analyzer 的类库,可以分析 Dart 代码。包括在上下文中的类继承信息、字段、方法等,这对于代码

注意 analyzer 分析的 dart 项目 ,必须 flutter pub get 一下,否则类型解析会不完整。

也就是说,首先需要在 SDK/packages/flutter/ 文件夹下执行 flutter pub get


3.代码实现

第一步,遍历源码文件夹,找出所有的 dart 文件:

String srcDir = p.join(sdkPath, 'packages', 'flutter', 'lib');              
                                                                                 
final List<String>  dartFiles = [];                                              
await for (FileSystemEntity entity in Directory(sdkPath).list(recursive: true)) {
  if (entity is File && entity.path.endsWith('.dart')) {                         
    dartFiles.add(entity.path);                                                  
  }                                                                              
}                                                                                

创建一个 RecursiveAstVisitor 访问器,在 visitClassDeclaration 回调中,可以访问类的声明信息。在其中可以得到其父类信息,通过 allSupertypes 可以获取全部的先祖类型。所有只需要校验其中是否存在名称为 StatelessWidget 的父类即可:

class StatelessWidgetVisitor extends RecursiveAstVisitor<void> {
  final String filePath;

  StatelessWidgetVisitor(this.filePath);

  final List<String> components = [];

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    String target = 'StatelessWidget';
    final String className = node.name.lexeme;
    List<InterfaceType>? types = node.declaredFragment?.element.allSupertypes;
    bool? hit = types?.where((e) => e.element3.name3 == target).isNotEmpty;
    if (hit ?? false) {
      components.add(className);
    }
    super.visitClassDeclaration(node);
  }
}                                                                             

AnalysisContextCollection分析上下文集合,用于解析 Dart 代码, 其中 includedPaths 是要分析的目录。然后遍历每个 Dart 文件进行分析,ResolvedUnitResult 表示解析 AST 成功。
接下来创建 StatelessWidgetVisitor,在 accept 时会让访问者遍历 AST,从而触发 visitClassDeclaration 执行收集工作:

final List<String> components = [];                                                             
final AnalysisContextCollection collection = AnalysisContextCollection(includedPaths: [srcDir]);
                                                                                                
for (String file in dartFiles) {                                                                
  try {                                                                                         
    AnalysisContext context = collection.contextFor(file);                                      
    SomeResolvedUnitResult result = await context.currentSession.getResolvedUnit(file);         
                                                                                                
    if (result is ResolvedUnitResult) {                                                         
      StatelessWidgetVisitor visitor = StatelessWidgetVisitor();                                
      result.unit.accept(visitor);                                                              
      components.addAll(visitor.components);                                                    
    }                                                                                           
  } catch (e) {                                                                                 
    // Skip files with errors                                                                   
  }                                                                                             
}                                                                                               

最后 components 列表就是收集的所有组件名,这里做了一个字母排序,并将其输出到了 stateless_components.md 中。这样就完成了第一版的 StatelesWidget 组件提取器:

components.sort((a,b)=>a.compareTo(b));
File outfile = File('stateless_components.md');
await outfile.writeAsString(components.join("\n"));
print('Results saved to: ${outfile.path}');

虽然输出了结果,不过目前的解析器功能还非常薄弱。接下来的题目将会逐步优化,以及收集更多的数据信息。通过解析可以全方位查看某个版本 Flutter SDK 组件的内容。


4. 小结

本题的核心,在于接触 如何用工程化的手段解析 SDK 并提取结构化信息。相比手工检索或靠阅读源码,本题所展示的方法借助 Dart 官方的 analyzer 工具链,实现了自动化分析和提取,非常适合做代码扫描、组件统计、依赖图谱等任务。

这正是《匠心星问》系列题库设计的初衷:不仅是“会不会用”,而是鼓励开发者去思考 “怎么做才更合理、更系统” 。希望你在解决本题的过程中,收获的不只是 Flutter 技术本身,更是一种解决问题的方式。

后续的《每日一题》也将持续围绕实战问题展开,欢迎持续关注和参与,一起构建一个 开放、精致、有深度 的 Flutter 题库生态!

你是否也有想出的题目,欢迎投稿加入《匠心星问》共同打造!


如果你有其他的看法,或者有什么想要的题目、或者想提供题目和答案,都欢迎在评论区留言。更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。