阅读 4334

Flutter 注解处理及代码生成

十九世纪中期一批与众不同的猿猴诞生了,他们排斥重复的工作,毕生都在追求效率和性能。而用代码去生成代码,是这些猴子的一点小聪明。

猴子说:“一家人就要整整齐齐!” 所以即使是新兴的Flutter,也被猴子们赋予了这样的能力。

本文首先将用一个简单的demo带你对Flutter,其实也就是 Dart 的注解处理和代码生成有一个初步的认识。

然后会对注解处理的各个环节和Api进行详细讲解,帮你去除初步认识过程中产生的各种疑惑,学会使用Dart注解处理。

为了简化描述,后文中[Dart注解处理],我们直接用 Dart-APT 表示。

再之后我们将会拿 Java-APT 与 Dart-APT 做一个对比,一方面强化你的认知,一方面介绍 Dart-APT 非常特殊的几个要点。

最后我们将对 Dart-APT 的 Generator 进行简要的源码分析,帮助你更深入的理解和使用Dart-APT。

本文大纲:

  • 1.初识 Dart-APT
  • 2.Dart-APT Api详解
  • 3.Java-APT & Dart-APT对比以及 Dart-APT 的特殊性
  • 4.Dart-APT Generator 源码浅析

初识 Dart 注解处理以及代码生成

第一节我先带你以最简单的demo,快速认识一下Flutter的注解处理和代码生成的样子,具体的API细节我们放后面细细道来。

Flutter,其实也就是Dart的注解处理依赖于 source_gen。它的详细资料可以在它的 Github 主页查看,这里我们不做过多展开,你只需要知道[ Dart-APT Powered by source_gen]

在Flutter中应用注解以及生成代码仅需一下几个步骤:

  • 1.依赖 source_gen
  • 2.创建注解
  • 3.创建生成器
  • 4.创建Builder
  • 5.编写配置文件

1.依赖 source_gen

第一步,在你工程的 pubspec.yaml 中引入 source_gen。如果你仅在本地使用且不打算将这个代码当做一个库发布出去:

dev_dependencies:
  source_gen:
复制代码

否则

dependencies:
  source_gen:
复制代码

2.创建注解和使用

比起 java 中的注解创建,Dart 的注解创建更加朴素,没有多余的关键字,实际上只是一个构造方法需要修饰成 const 的普通 Class 。

例如,申明一个没有参数的注解:

class TestMetadata {
  const TestMetadata();
}
复制代码

使用:

@TestMetadata()
class TestModel {}
复制代码

申明一个有参数的注解:

class ParamMetadata {
  final String name;
  final int id;

  const ParamMetadata(this.name, this.id);
}

复制代码

使用:

@ParamMetadata("test", 1)
class TestModel {}
复制代码

3.创建生成器

类似 Java-APT 的 Processor ,在 Dart 的世界里,具有相同职责的是 Generator。

你需要创建一个 Generator,继承于 GeneratorForAnnotation, 并实现: generateForAnnotatedElement 方法。

还要在 GeneratorForAnnotation 的泛型参数中填入我们要拦截的注解。

class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
  @override
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    return "class Tessss{}";
  }
}
复制代码

返回值是一个 String,其内容就是你将要生成的代码。

你可以通过 generateForAnnotatedElement 方法的三个参数获取注解的各种信息,用来生成相对应的代码。三个参数的具体使用我们后面细讲。

这里我们仅简单的返回一个字符串 "class Tessss{}",用来看看效果。

4.创建Builder

Generator 的执行需要 Builder 来触发,所以现在我们要创建一个Builder。

非常简单,只需要创建一个返回类型为 Builder 的全局方法即可:

Builder testBuilder(BuilderOptions options) =>
    LibraryBuilder(TestGenerator());
复制代码

方法名随意,重点需要关注的是返回的对象。

示例中我们返回的是 LibraryBuilder 对象,构造方法的参数是我们上一步创建的TestGenerator对象。

实际上根据不同的需求,我们还有其他Builder对象可选,Builder 的继承树:

  • Builder
    • _Builder
      • PartBuilder
      • LibraryBuilder
      • SharedPartBuilder
    • MultiplexingBuilder

PartBuilder 与 SharedPartBuilder 涉及到 dart-part 关键字的使用,这里我们暂时不做展开,通常情况下 LibraryBuilder 已足以满足我们的需求。 MultiplexingBuilder 支持多个Builder的添加。

5.创建配置文件

在项目根目录创建 build.yaml 文件,其意义在于 配置 Builder 的各项参数:

builders:
  testBuilder:
    import: "package:flutter_annotation/test.dart"
    builder_factories: ["testBuilder"]
    build_extensions: {".dart": [".g.part"]}
    auto_apply: root_package
    build_to: source
复制代码

配置信息的详细含义我们后面解释。重点关注的是,通过 import 和 builder_factories 两个标签,我们指定了上一步创建的 Builder。

6.运行 Builder

命令行中执行命令,运行我们的 Builder

$ flutter packages pub run build_runner build
复制代码

受限于Flutter 禁止反射的缘故,你不能再像Android中使用编译时注解那样,coding 阶段使用接口,编译阶段生成实现类,运行阶段通过反射创建实现类对象。在Flutter中,你只能先通过命令生成代码,然后再直接使用生成的代码。

可以看到命令还是偏长的,一个可行的建议是将命令封装成一个脚本。

不出意外的话,命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// TestGenerator
// **************************************************************************

class Tessss {}

复制代码

代码生成成功!

清理生成的文件无需手动删除,可执行以下命令:

flutter packages pub run build_runner clean
复制代码

Dart-APT Api详解

  • 1.注解创建与使用
  • 2.创建生成器 Generator
  • 3.generateForAnnotatedElement 参数: element
  • 4.generateForAnnotatedElement 参数: annotation
  • 5.generateForAnnotatedElement 参数: buildStep
  • 6.模板代码生成技巧
  • 7.配置文件字段含义

1.注解创建与使用

Dart的注解创建和普通的class创建没有任何区别,可以 extends, 可以 implements ,甚至可以 with。

唯一必须的要求是:构造方法需要用 const 来修饰。

不同于java注解的创建需要指明@Target(定义可以修饰对象范围)

Dart 的注解没有修饰范围,定义好的注解可以修饰类、属性、方法、参数。

但值得注意的是,如果你的 Generator 直接继承自 GeneratorForAnnotation, 那你的 Generator 只能拦截到 top-level 级别的元素,对于类内部属性、方法等无法拦截,类内部属性、方法修饰注解暂时没有意义。(不过这个事情扩展一下肯定可以实现的啦~)

2.创建生成器 Generator

Generator 为创建代码而生。通常情况下,我们将继承 GeneratorForAnnotation,并在其泛型参数中添加目标 annotation。然后复写 generateForAnnotatedElement 方法,最终 return 一个字符串,便是我们要生成的代码。

class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
  @override
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    return "class Tessss{}";
  }
}
复制代码

GeneratorForAnnotation的注意点有:

2.1 GeneratorForAnnotation与annotation的对应关系

GeneratorForAnnotation是单注解处理器,每一个 GeneratorForAnnotation 必须有且只有一个 annotation 作为其泛型参数。也就是说每一个继承自GeneratorForAnnotation的生成器只能处理一种注解。

2.2 generateForAnnotatedElement 参数含义

最值得关注的是 generateForAnnotatedElement 方法的三个参数:Element element, ConstantReader annotation, BuildStep buildStep。我们生成代码所依赖的信息均来自这三个参数。

  • Element element:被 annotation 所修饰的元素,通过它可以获取到元素的name、可见性等等。
  • ConstantReader annotation:表示注解对象,通过它可以获取到注解相关信息以及参数值。
  • BuildStep buildStep:这一次构建的信息,通过它可以获取到一些输入输出信息,例如输入文件名等

generateForAnnotatedElement 的返回值是一个 String,你需要用字符串拼接出你想要生成的代码,return null 意味着不需要生成文件。

2.3 代码与文件生成规则

不同于java apt,文件生成完全由开发者自定义。GeneratorForAnnotation 的文件生成有一套自己的规则。

在不做其他深度定制的情况下,如果 generateForAnnotatedElement 的返回值 永不为空,则:

  • 若一个源文件仅含有一个被目标注解修饰的类,则每一个包含目标注解的文件,都对应一个生成文件;

  • 若一个源文件含有多个被目标注解修饰的类,则生成一个文件,generateForAnnotatedElement方法被执行多次,生成的代码通过两个换行符拼接后,输出到该文件中。

3.generateForAnnotatedElement 参数: Element

例如我们有这样一段代码,使用了 @TestMetadata 这个注解:

@ParamMetadata("ParamMetadata", 2)
@TestMetadata("papapa")
class TestModel {
  int age;
  int bookNum;

  void fun1() {}

  void fun2(int a) {}
}

复制代码

在 generateForAnnotatedElement 方法中,我们可以通过 Element 参数获取 TestModel 的一些简单信息:

element.toString: class TestModel
element.name: TestModel
element.metadata: [@ParamMetadata("ParamMetadata", 2),@TestMetadata("papapa")] 
element.kind: CLASS
element.displayName: TestModel
element.documentationComment: null
element.enclosingElement: flutter_annotation|lib/demo_class.dart
element.hasAlwaysThrows: false
element.hasDeprecated: false
element.hasFactory: false
element.hasIsTest: false
element.hasLiteral: false
element.hasOverride: false
element.hasProtected: false
element.hasRequired: false
element.isPrivate: false
element.isPublic: true
element.isSynthetic: false
element.nameLength: 9
element.runtimeType: ClassElementImpl
...
复制代码

由前文我们知道,GeneratorForAnnotation的域仅限于class, 通过 element 只能拿到 TestModel 的类信息,那类内部的 Field 和 method 信息如何获取呢?

关注 kind 属性值: element.kind: CLASS,kind 标识 Element 的类型,可以是 CLASS、FIELD、FUNCTION 等等。

对应这些类型,还有相应的 Element 子类:ClassElement、FieldElement、FunctionElement等等,所以你可以这样:

if(element.kind == ElementKind.CLASS){
  for (var e in ((element as ClassElement).fields)) {
    print("$e \n");
  }
  for (var e in ((element as ClassElement).methods)) {
	print("$e \n");
  }
}

输出:
int age 
int bookNum 
fun1() → void 
fun2(int a) → void 
    
复制代码

4.generateForAnnotatedElement 参数: annotation

注解除了标记以外,携带参数也是注解很重要的能力之一。注解携带的参数,可以通过 annotation 获取:

annotation.runtimeType: _DartObjectConstant
annotation.read("name"): ParamMetadata
annotation.read("id"): 2
annotation.objectValue: ParamMetadata (id = int (2); name = String ('ParamMetadata'))
复制代码

annotation 的类型是 ConstantReader,除了提供 read 方法来获取具体参数以外,还提供了peek方法,它们两个的能力相同,不同之处在于,如果read方法读取了不存在的参数名,会抛出异常,peek则不会,而是返回null。

5.generateForAnnotatedElement 参数: buildStep

buildStep 提供的是该次构建的输入输出信息:

buildStep.runtimeType: BuildStepImpl
buildStep.inputId.path: lib/demo_class.dart
buildStep.inputId.extension: .dart
buildStep.inputId.package: flutter_annotation
buildStep.inputId.uri: package:flutter_annotation/demo_class.dart
buildStep.inputId.pathSegments: [lib, demo_class.dart]
buildStep.expectedOutputs.path: lib/demo_class.g.dart
buildStep.expectedOutputs.extension: .dart
buildStep.expectedOutputs.package: flutter_annotation
复制代码

6.模板代码生成技巧

现在,你已经获取了所能获取的三个信息输入来源,下一步则是根据这些信息来生成代码。

如何生成代码呢?你有以下两个选择:

6.1 简单模板代码,字符串拼接:

如果需要生成的代码不是很复杂,则可以直接用字符串进行拼接,比如这样:

generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    ...
    StringBuffer codeBuffer = StringBuffer("\n");
    codeBuffer..write("class ")
      ..write(element.name)
      ..write("_APT{")
      ..writeln("\n")
      ..writeln("}");
    
    return codeBuffer.toString();
  }
复制代码

不过一般情况下我们并不建议这样做,因为这样写起来太容易出错了,且不具备可读性。

6.2 复杂模板代码,dart 多行字符串+占位符

dart提供了一种三引号的语法,用于多行字符串:

var str3 = """大王叫我来巡山
  路口遇见了如来
  """;
复制代码

结合占位符后,可以实现比较清晰的模板代码:

tempCode(String className) {
    return """
      class ${className}APT {
 
      }
      """;
  }
  
generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    ...
    return tempCode(element.name);
  } 

复制代码

如果参数过多的话,tempCode方法的参数可以替换为一个Map。

(在模板代码中不要忘记import package哦~ 建议先在编译器里写好模板代码,编译器静态检查没有问题了,再放到三引号中修改占位符)

如果你熟悉java-apt的话,看到这里应该会想问,dart里有没有类似 javapoet 这样的代码库来辅助生成代码啊?从个人角度来说,更推荐第二种方式去生成代码,因为它表现的足够清晰,具有足够高的可读性,比起javapoet这种模式,可以更容易的理解模板代码意义,编写也更加简单。

7.配置文件字段含义

在工程根目录下创建build.yaml 文件,用来配置Builder相关信息。

以下面配置为例:

builders:
  test_builder:
    import: 'package:flutter_annotation/test_builder.dart'
    builder_factories: ['testBuilder']
    build_extensions: { '.dart': ['.g1.dart'] }
    required_inputs:['.dart']
    auto_apply: root_package
    build_to: source

  test_builder2:
    import: 'package:flutter_annotation/test_builder2.dart'
    builder_factories: ['testBuilder2']
    build_extensions: { '.dart': ['.g.dart'] }
    auto_apply: root_package
    runs_before: ['flutter_annotation|test_builder']
    build_to: source
复制代码

builders 下配置你所有的builder。test_builder与 test_builder2 均是你的builder命名。

  • import 关键字用于导入 return Builder 的方法所在包 (必须)
  • builder_factories 填写的是我们 return Builder 的方法名(必须)
  • build_extensions 指定输入扩展名到输出扩展名的映射,比如我们接受.dart文件的输入,最终输出.g.dart 文件(必须)
  • auto_apply 指定builder作用于,可选值: (可选,默认为 none)
    • "none":除非手动配置,否则不要应用此Builder
    • "dependents":将此Builder应用于包,直接依赖于公开构建器的包。
    • "all_packages":将此Builder应用于传递依赖关系图中的所有包。
    • "root_package":仅将此Builder应用于顶级包。
  • build_to 指定输出位置,可选值: (可选,默认为 cache)
    • "source": 输出到其主要输入的源码树上
    • "cache": 输出到隐藏的构建缓存上
  • required_inputs 指定一个或一系列文件扩展名,表示在任何可能产生该类型输出的Builder之后运行(可选)
  • runs_before 保证在指定的Builder之前运行

配置字段的解释较为拗口,这里我只列出了常用的一些配置字段,还有一些不常用的字段可以在 source_gen 的github主页 查阅。

Java-APT & Dart-APT对比以及 Dart-APT 的特殊性

下面我们将列出 Java-APT 和 Dart-APT 的主要区别,做一下对比,以此加深你的理解和提供注意事项。

1.注解定义

Java-APT: 需在定义注解时指定注解被解析时机(编码阶段、源码阶段、运行时阶段),以及注解作用域(类、方法、属性)

Dart-APT: 无需指定注解被解析时机以及注解作用域,默认 Anytime and anywhere

2.注解与注解处理器的关系

Java-APT: 一个注解处理器可以指定多个注解进行处理

Dart-APT: 使用 source_gen 提供的默认处理器: GeneratorForAnnotation ,一个处理器只能处理一个注解。

3.注解拦截范围

Java-APT: 每一个合法使用的注解均可以被注解处理器拦截。

Dart-APT: 使用 source_gen 提供的默认处理器: GeneratorForAnnotation ,处理器只能处理 top-level级别的元素,例如直接在.dart 文件定义的Class、function、enums等等,但对于类内部Fields、functions 上使用的注解则无法拦截。

4.注解与生成文件的关系

Java-APT: 注解和生成文件的个数并无直接关系,开发者自行定义

Dart-APT: 在注解处理器返回值不为空的情况下,通常一个输入文件对应一个输出文件,如果不想生成文件,只需要在Generate的方法中return null即可 。若一个输入文件包含多个注解,每个成功被拦截到的注解都会触发generateForAnnotatedElement 方法的调用,多次触发而得到的返回值,最终会写入到同一个文件当中。

5.注解处理器之间的运行顺序

Java-APT: 无法直接指定多个处理器之间的执行顺序

Dart-APT: 可以指定多个处理器之间的执行顺序,在配置文件build.yaml中指定key值 runs_beforerequired_inputs

6.多个注解信息合并处理

Java-APT: 注解处理器指定多个需要处理的注解后,可以在信息采集结束后统一处理

Dart-APT: 默认一个处理器只能处理一个注解,想要合并处理需指定处理器的执行顺序,先执行的注解处理器负责不同类型注解的信息采集(采集的数据可以用静态变量保存),最后执行的处理器负责处理之前保存好的数据。

第3、第4点与Java-APT非常不一样,你可能还有点懵,这里用一个栗子来说明:

栗子

假设我们有两个文件:

example.dart

@ParamMetadata("ClassOne", 1)
class One {
  @ParamMetadata("field1", 2)
  int age;
  @ParamMetadata("fun1", 3)
  void fun1() {}
}

@ParamMetadata("ClassTwo", 4)
class Two {
  int age;
  void fun1() {}
}
复制代码

example1.dart

@ParamMetadata("ClassThree", 5)
class Three {
  int age;
  void fun1() {}
}

复制代码

Generate实现如下:

class TestGenerator extends GeneratorForAnnotation<ParamMetadata> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    print("当前输入源: ${buildStep.inputId.toString()}  被拦截到的元素: ${element.name} 注解值: ${annotation.read("name").stringValue} ${annotation.read("id").intValue}");
    return tempCode(element.name);
  }

  tempCode(String className) {
    return """
      class ${className}APT {
      }
      """;
  }
}
复制代码

执行 flutter packages pub run build_runner build

控制台输出信息:

当前输入源: flutter_annotation|lib/example.dart  被拦截到的元素: One 注解值: ClassOne 1
当前输入源: flutter_annotation|lib/example.dart  被拦截到的元素: Two 注解值: ClassTwo 4
当前输入源: flutter_annotation|lib/example1.dart  被拦截到的元素: Three 注解值: ClassThree 5
复制代码

生成的文件:

- lib
	- example.dart
	- example.g.dart
	- example.dart
	- example1.g.dart
复制代码

example.g.dart

class OneAPT {}

class TwoAPT {}
复制代码

example1.g.dart

class ThreeAPT {}
复制代码

栗子总结

在文件 example.dart 中,我们有两个Class使用了注解,其中一个Class除了Class本身以外,它的field 和 function 也使用了注解。

但在输出中,我们只拦截到了 ClassOne, 并没有被拦截到 field1 fun1。

这解释了:

  • library.annotatedWith 遍历的 Element 仅包括top-level级别的 Element,也就是那些文件级别的 Class、function等等,而Class 内部的 fields、functions并不在遍历范围,如果在 Class 内部的fields 或 functions 上修饰注解,GeneratorForAnnotation并不能拦截到!

生成的 .g.dart 文件当中,因为Class One 和 Class Two 都在文件 example.dart 中,所以生成的代码也都拼接在了文件example.g.dart中。

这解释了:

  • 若一个输入文件包含多个注解,每个成功被拦截到的注解都会触发 generateForAnnotatedElement 方法的调用,多次触发而得到的返回值,最终会写入到同一个文件当中。

另外一个文件example1.dart 则单独生成了文件 example1.g.dart

这解释了:

  • 当返回值不为空的情况下,每一个文件输入源对应着一个文件输出。也就是说源码中,每一个*.dart文件都会触发一次generate方法调用,如果返回值不为空,则输出一个文件。

Dart-APT Generator 源码浅析

1.Generator 源码浅析

Generator源码炒鸡炒鸡简单:

abstract class Generator {
  const Generator();

  /// Generates Dart code for an input Dart library.
  ///
  /// May create additional outputs through the `buildStep`, but the 'primary'
  /// output is Dart code returned through the Future. If there is nothing to
  /// generate for this library may return null, or a Future that resolves to
  /// null or the empty string.
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) => null;

  @override
  String toString() => runtimeType.toString();
}
复制代码

就这么几行代码,在 Builder 运行时,会调用 Generator 的 generate方法,并传入两个重要的参数:

  • library 通过它,我们可以获取源代码信息以及注解信息
  • buildStep 它表示构建过程中的一个步骤,通过它,我们可以获取一些文件的输入输出信息

值得注意的是,library 包含的源码信息是一个个的 Element 元素,这些 Element 可以是Class、可以是function、enums等等。

ok,让我们再来看看 source_gen 中,Generator 的唯一子类 :GeneratorForAnnotation 的源码:

abstract class GeneratorForAnnotation<T> extends Generator {
  const GeneratorForAnnotation();

  //1   typeChecker 用来做注解检查
  TypeChecker get typeChecker => TypeChecker.fromRuntime(T);

  @override
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async {
    var values = Set<String>();

    //2  遍历所有满足 注解 类型条件的element
    for (var annotatedElement in library.annotatedWith(typeChecker)) {
      //3 满足检查条件的调用 generateForAnnotatedElement 执行开发者自定义的代码生成逻辑
      var generatedValue = generateForAnnotatedElement(
          annotatedElement.element, annotatedElement.annotation, buildStep);
          //4 generatedValue是将要生成的代码字符串,通过normalizeGeneratorOutput格式化
      await for (var value in normalizeGeneratorOutput(generatedValue)) {
        assert(value == null || (value.length == value.trim().length));
        //5 生成的代码加入集合
        values.add(value);
      }
    }
	//6
    return values.join('\n\n');
  }
	
	//7
  generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep);
复制代码
  • //1 : typeChecker 用来做注解检查,效验Element上是否修饰了目标注解
  • //2 : library.annotatedWith(typeChecker) 会遍历所有的 Element,并通过typeChecker检查这些Element 是否修饰了目标注解。值得再次说明的是:library.annotatedWith 遍历的 Element 仅包括top-level级别的 Element,也就是那些文件级别的 Class、function等等,而Class 内部的 fields、functions并不在遍历范围,如果在 Class 内部的fields 或 functions 上修饰注解,GeneratorForAnnotation并不会拦截到!
  • //3 : 满足条件后,调用generateForAnnotatedElement方法,也就是我们自定义Generator所实现的抽象方法。
  • //4 : generatedValue 是generateForAnnotatedElement返回值,也是我们要生成的代码,调用normalizeGeneratorOutput去做格式化。
  • //5 : 满足条件后,添加到集合values当中。值得再次说明的是: 之前我们也提到过,当返回值不为空的情况下,每一个文件输入源对应着一个文件输出。也就是说源码中,每一个*.dart文件都会触发一次generate方法调用,而其中每一个符合条件的目标注解使用,都会触发一次generateForAnnotatedElement 调用,如果被多次调用,多个返回值最终会拼接起来,输出到一个文件当中。
  • //6 : 每个单独的输出之间用两个换行符分割,最终输出到一个文件当中。
  • //7 : 我们自定义Generator所实现的抽象方法。

2.library.annotatedWith 源码浅析

GeneratorForAnnotation的源码也很简单,唯一值得关注的是 library.annotatedWith方法,我们看看它的源码:

class LibraryReader {
  final LibraryElement element;
  //1 element输入源,这里容易产生误解
  LibraryReader(this.element);

  ...

  //2 所有Element,但仅限top-level级别
  Iterable<Element> get allElements sync* {
    for (var cu in element.units) {
      yield* cu.accessors;
      yield* cu.enums;
      yield* cu.functionTypeAliases;
      yield* cu.functions;
      yield* cu.mixins;
      yield* cu.topLevelVariables;
      yield* cu.types;
    }
  }

  Iterable<AnnotatedElement> annotatedWith(TypeChecker checker,
      {bool throwOnUnresolved}) sync* {
    for (final element in allElements) {
      //3 如果修饰了多个相同的注解,只会取第一个
      final annotation = checker.firstAnnotationOf(element,
          throwOnUnresolved: throwOnUnresolved);
      if (annotation != null) {
        //4 将annotation包装成AnnotatedElement对象返回
        yield AnnotatedElement(ConstantReader(annotation), element);
      }
    }
  }

复制代码
  • //1 : Element对象是很标准的组合模式,这里容易产生的误解:这个Element,是被应用的项目中,所有源代码的的一个根 Element。这是错误的,正确的答案是:这个Element和其子元素,所包含的范围,仅限一个文件。
  • //2 : 这里的 allElements 仅限top-level级别的子Element
  • //3 : 这里会借助 checker 检查 Element 所修饰的注解,如果修饰了多个相同的注解,只会取第一个,如果没有目标注解,则返回null
  • //4 : 返回的 annotation 实际只是一个 DartObject 对象,可以通过这个对象来取值,但为了便于使用,这里要将它再包装成API更友好的AnnotatedElement,然后返回。

总结

好啦~ 到这里你已经对 Dart-APT 有一个初步的认识了,应该具有使用 Dart-APT 的提高开发效率的能力了! APT 本身并不难,难的是利用 APT 的创意!期待你的想法与创作!

哦对了~ 全篇看下来,你应该会发现 Dart-APT 与 Java-APT 相比,它的实现还是比较特殊的,对比 Java-APT,好多能力都暂不具备或实现起来比较繁琐,我们整理下哦:

  • 无法拦截在类内部 属性、方法上等使用的注解
  • 一个注解处理器只能处理一个注解
  • 没有直接的API自定义文件生成等等
  • 多注解信息合并处理较为繁琐

另外通过阅读 Generate 源码,我们还意识到有一些能力 Dart-APT 可以实现但 Java-APT 不好实现:

  • 直接拦截某一个 Class 或 所有继承自该 Class 的子类,而不使用注解。

Flutter还是一个新兴技术, source_gen 目前只提供了最基础的APT能力,上面的这些功能的实现并不是不能,而只是时间或ROI的问题了。

后面计划针对这些功能,产出一个 Dart-APT 扩展库,期待一下吧 (^__^)~

关注下面的标签,发现更多相似文章
评论