自定义生成器 (Generator)

Builder 负责文件系统的操作(读取 .dart 文件,创建 .g.dart 文件),其逻辑比较底层,写起来稍显繁琐。Generator 封装了 Builder,让我们只需关注“输入一个类,输出一段字符串”的逻辑。绝大多数场景下,我们只需自定义 Generator。

示例

假设我们要为 OMDb 模型类(如 Movie 类)自动生成一个字段清单字符串。

a. Generator 由 source_gen 包提供。首先添加如下依赖包:

dart pub add source_gen analyzer build

b. 定义注解(OMDbModelInfo).

// lib/codegen/annotations.dart
class OMDbModelInfo {
  const OMDbModelInfo();
}

c. 定义基于注解(OMDbModelInfo)的Generator(ModelInfoGenerator)。

// lib/codegen/model_info_generator.dart
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'annotations.dart';

class ModelInfoGenerator extends GeneratorForAnnotation<OMDbModelInfo> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    log.info('>>>>>>>> 正在处理类: ${element.name} <<<<<<<<');

    // 确保注解是在类上
    if (element is! ClassElement) throw 'Only classes!';

    final className = element.name;
    // 获取所有字段名
    final fields = element.fields.map((f) => f.name).join(', ');

    // 返回的字符串会直接写入到 .g.dart 中
    return '''
// ignore: unused_element
const _\$${className}Fields = "$fields";
''';
  }
}

d. 创建一个全局函数(omdbModelBuilder),该函数返回一个 Builder

// lib/codegen/builder.dart
import 'package:build/build.dart';
import 'package:hellodart/codegen/model_info_generator.dart';
import 'package:source_gen/source_gen.dart';

Builder omdbModelBuilder(BuilderOptions options) =>
    PartBuilder([ModelInfoGenerator()], '.info.g.dart');

这里指定生成文件的后缀:.info.g.dart

e. 配置 build.yaml

builders:
  omdb_model_builder:
    import: "package:hellodart/codegen/builder.dart"
    # 指向一个返回 Builder 的顶层函数
    builder_factories: ["omdbModelBuilder"]
    # 规定:输入 .dart 文件,输出 .info.g.dart 文件
    build_extensions: {".dart": [".info.g.dart"]}
    auto_apply: dependents
    build_to: source

# targets 用于定义构建范围和具体插件的行为
targets:
  $default: 
    builders:
      hellodart|omdb_model_builder:
        generate_for:
          - bin/ch09/*.dart
  • 根部的 builders 是声明(Declaration),targets 里的 builders 是引用(Reference)。
  • hellodart|omdb_model_builder: 这里的语法是 packageName|builderName。如果 packageNamebuilderName 相同,可以只写 builderName。上一节中的 json_serializable 的全名是 json_serializable|json_serializable。这种设计体现了 Dart 构建系统的模块化哲学。

f. 生成代码.

修改 movie.dart

part 'movie.g.dart';
part 'movie.info.g.dart';

@OMDbModelInfo()
@JsonSerializable()
class Movie {
// ... (omited for brevity)
}

然后运行 build 命令:

dart run build_runner build

Builder 类层次

从 Builder的类图可以看出 Builder 与 Generator 的关系。

classDiagram
    class Builder {
        <<abstract>>
        +Map buildExtensions
        +build(BuildStep buildStep)*
    }

    class PartBuilder {
        +List generators
        +String formatOutput
        +build(BuildStep buildStep)
    }

    class SharedPartBuilder {
        +List generators
        +String partId
        +build(BuildStep buildStep)
    }

    class LibraryBuilder {
        +Generator generator
        +build(BuildStep buildStep)
    }

    class Generator {
        <<abstract>>
        +generate(LibraryReader library, BuildStep buildStep)*
    }

    class GeneratorForAnnotation~T~ {
        <<abstract>>
        +generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep)*    
    }

    %% 关系
    Builder <|-- PartBuilder : 继承 (独立生成文件)
    Builder <|-- SharedPartBuilder : 继承 (生成到 .g.dart)
    Builder <|-- LibraryBuilder : 继承 (生成整个库)

    PartBuilder o-- Generator : 持有 (1..*)
    SharedPartBuilder o-- Generator : 持有 (1..*)
    LibraryBuilder o-- Generator : 持有 (1)

    Generator <|-- GeneratorForAnnotation : 继承 (基于注解触发)

ConstantReader

当我们写下 @JsonSerializable(explicitToJson: true) 时,explicitToJson 这个布尔值是如何传递给Generator的?这就是 ConstantReader 的工作,它负责将源码中的“静态常量”转换为 Generator 可以直接操作的 Dart 对象。

继续之前的示例,设想我们要为 OMDb模型类自动生成一个扩展方法 printFields,该方法打印模型类的所有字段。我们向 OMDbModelInfo 注解添加一个布尔开关 ext,只有当开发者明确声明 ext: true 时,Generator 才生成 printFields

首先修改 OMDbModelInfo,添加 ext 属性.

// lib/codegen/annotations.dart
class OMDbModelInfo {
  final bool ext;
  const OMDbModelInfo({this.ext = false});
}

然后我们来看看如何读区 ext 属性。创建一个新的 Generator (ModelExtGenerator)。

// lib/codegen/model_ext_generator.dart
// import ... (omited for brevity)
class ModelExtGenerator extends GeneratorForAnnotation<OMDbModelInfo> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) throw 'Only classes!';

    final ext = annotation.read('ext').boolValue;
    if (!ext) return "";

    return "// TODO";
  }
}

annotation.read('ext') 读取 ext属性,如果该属性不存在将报错。还可以使用 peek('prop')读取属性,这种方式具有更高的容错性 — 如果属性不存在,它将返回 null,适用于可选属性(配置)。

    final ext = annotation.peek('ext')?.boolValue ?? false;

code_builder

在之前的 ModelInfoGenerator 实战中,我们使用了简单的字符串拼接(return 'const ...')。但在面对复杂的类结构、导入管理、私有字段时,这简直是开发者的噩梦。而 code_builder 具有如下 自动导入、代码格式化、语义化编程等特性,极大地简化了 复杂Generator 的编写。字符串拼接如同剪纸拼图,琐碎且易错;而 code_builder 则是语义化的乐高,它用对象化的组件替代了原始的文本堆砌,让代码生成变得严谨且优雅

接下来使用 code_builder 来实现 ModelExtGenerator的具体逻辑,以演示其用法。

// lib/codegen/model_ext_generator.dart
// import ... (omited for brevity)
class ModelExtGenerator extends GeneratorForAnnotation<OMDbModelInfo> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) throw 'Only classes!';

    final ext = annotation.read('ext').boolValue;
    if (!ext) return "";

    final className = element.name!;

    // 使用 code_builder 构建代码对象
    final extension = Extension(
      (e) => e
        ..name = '${className}Ext'
        // 绑定到原类
        ..on = refer(className)
        ..methods.add(
          Method(
            (m) => m
              ..name = 'printFields'
              ..returns = refer('void')
              ..body = Block.of([
                // 自动生成打印逻辑
                refer(
                  'print',
                ).call([refer('_\$${className}Fields')]).statement,
              ]),
          ),
        ),
    );

    // 使用 DartEmitter 将对象发射(Emit)为字符串
    final emitter = DartEmitter();
    return '${extension.accept(emitter)}';
  }
}

修改 omdbModelBuilder 全局函数:

Builder omdbModelBuilder(BuilderOptions options) =>
    PartBuilder([ModelInfoGenerator(), ModelExtGenerator()], '.info.g.dart');

最后,在 Movie 类上使用 @OMDbModelInfo(ext: true) ,它会自动生成:

extension MovieExt on Movie {
  void printFields() {
    print(_$MovieFields);
  }
}

Reference