十九世纪中期一批不同凡响的猿猴诞生了,他们排斥重复的工做,毕生都在追求效率和性能。而用代码去生成代码,是这些猴子的一点小聪明。java
猴子说:“一家人就要整整齐齐!” 因此即便是新兴的Flutter,也被猴子们赋予了这样的能力。git
本文首先将用一个简单的demo带你对Flutter,其实也就是 Dart 的注解处理和代码生成有一个初步的认识。github
而后会对注解处理的各个环节和Api进行详细讲解,帮你去除初步认识过程当中产生的各类疑惑,学会使用Dart注解处理。缓存
为了简化描述,后文中[Dart注解处理],咱们直接用 Dart-APT 表示。bash
再以后咱们将会拿 Java-APT 与 Dart-APT 作一个对比,一方面强化你的认知,一方面介绍 Dart-APT 很是特殊的几个要点。app
最后咱们将对 Dart-APT 的 Generator 进行简要的源码分析,帮助你更深刻的理解和使用Dart-APT。async
本文大纲:ide
第一节我先带你以最简单的demo,快速认识一下Flutter的注解处理和代码生成的样子,具体的API细节咱们放后面细细道来。源码分析
Flutter,其实也就是Dart的注解处理依赖于 source_gen。它的详细资料能够在它的 Github 主页查看,这里咱们不作过多展开,你只须要知道[ Dart-APT Powered by source_gen]性能
在Flutter中应用注解以及生成代码仅需一下几个步骤:
第一步,在你工程的 pubspec.yaml 中引入 source_gen。若是你仅在本地使用且不打算将这个代码当作一个库发布出去:
dev_dependencies:
source_gen:
复制代码
不然
dependencies:
source_gen:
复制代码
比起 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 {}
复制代码
相似 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{}",用来看看效果。
Generator 的执行须要 Builder 来触发,因此如今咱们要建立一个Builder。
很是简单,只须要建立一个返回类型为 Builder 的全局方法便可:
Builder testBuilder(BuilderOptions options) =>
LibraryBuilder(TestGenerator());
复制代码
方法名随意,重点须要关注的是返回的对象。
示例中咱们返回的是 LibraryBuilder 对象,构造方法的参数是咱们上一步建立的TestGenerator对象。
实际上根据不一样的需求,咱们还有其余Builder对象可选,Builder 的继承树:
PartBuilder 与 SharedPartBuilder 涉及到 dart-part 关键字的使用,这里咱们暂时不作展开,一般状况下 LibraryBuilder 已足以知足咱们的需求。 MultiplexingBuilder 支持多个Builder的添加。
在项目根目录建立 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。
命令行中执行命令,运行咱们的 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的注解建立和普通的class建立没有任何区别,能够 extends, 能够 implements ,甚至能够 with。
惟一必须的要求是:构造方法须要用 const 来修饰。
不一样于java注解的建立须要指明@Target(定义能够修饰对象范围)
Dart 的注解没有修饰范围,定义好的注解能够修饰类、属性、方法、参数。
但值得注意的是,若是你的 Generator 直接继承自 GeneratorForAnnotation, 那你的 Generator 只能拦截到 top-level 级别的元素,对于类内部属性、方法等没法拦截,类内部属性、方法修饰注解暂时没有意义。(不过这个事情扩展一下确定能够实现的啦~)
Generator 为建立代码而生。一般状况下,咱们将继承 GeneratorForAnnotation,并在其泛型参数中添加目标 annotation。而后复写 generateForAnnotatedElement 方法,最终 return 一个字符串,即是咱们要生成的代码。
class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "class Tessss{}";
}
}
复制代码
GeneratorForAnnotation的注意点有:
GeneratorForAnnotation是单注解处理器,每个 GeneratorForAnnotation 必须有且只有一个 annotation 做为其泛型参数。也就是说每个继承自GeneratorForAnnotation的生成器只能处理一种注解。
最值得关注的是 generateForAnnotatedElement 方法的三个参数:Element element, ConstantReader annotation, BuildStep buildStep
。咱们生成代码所依赖的信息均来自这三个参数。
generateForAnnotatedElement 的返回值是一个 String,你须要用字符串拼接出你想要生成的代码,return null
意味着不须要生成文件。
不一样于java apt,文件生成彻底由开发者自定义。GeneratorForAnnotation 的文件生成有一套本身的规则。
在不作其余深度定制的状况下,若是 generateForAnnotatedElement 的返回值 永不为空,则:
若一个源文件仅含有一个被目标注解修饰的类,则每个包含目标注解的文件,都对应一个生成文件;
若一个源文件含有多个被目标注解修饰的类,则生成一个文件,generateForAnnotatedElement方法被执行屡次,生成的代码经过两个换行符拼接后,输出到该文件中。
例如咱们有这样一段代码,使用了 @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
复制代码
注解除了标记之外,携带参数也是注解很重要的能力之一。注解携带的参数,能够经过 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。
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
复制代码
如今,你已经获取了所能获取的三个信息输入来源,下一步则是根据这些信息来生成代码。
如何生成代码呢?你有如下两个选择:
若是须要生成的代码不是很复杂,则能够直接用字符串进行拼接,好比这样:
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();
}
复制代码
不过通常状况下咱们并不建议这样作,由于这样写起来太容易出错了,且不具有可读性。
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这种模式,能够更容易的理解模板代码意义,编写也更加简单。
在工程根目录下建立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命名。
.dart
文件的输入,最终输出.g.dart
文件(必须)配置字段的解释较为拗口,这里我只列出了经常使用的一些配置字段,还有一些不经常使用的字段能够在 source_gen 的github主页 查阅。
下面咱们将列出 Java-APT 和 Dart-APT 的主要区别,作一下对比,以此加深你的理解和提供注意事项。
Java-APT: 需在定义注解时指定注解被解析时机(编码阶段、源码阶段、运行时阶段),以及注解做用域(类、方法、属性)
Dart-APT: 无需指定注解被解析时机以及注解做用域,默认 Anytime and anywhere
Java-APT: 一个注解处理器能够指定多个注解进行处理
Dart-APT: 使用 source_gen 提供的默认处理器: GeneratorForAnnotation ,一个处理器只能处理一个注解。
Java-APT: 每个合法使用的注解都可以被注解处理器拦截。
Dart-APT: 使用 source_gen 提供的默认处理器: GeneratorForAnnotation ,处理器只能处理 top-level级别的元素,例如直接在.dart
文件定义的Class、function、enums等等,但对于类内部Fields、functions 上使用的注解则没法拦截。
Java-APT: 注解和生成文件的个数并没有直接关系,开发者自行定义
Dart-APT: 在注解处理器返回值不为空的状况下,一般一个输入文件对应一个输出文件,若是不想生成文件,只须要在Generate的方法中return null
便可 。若一个输入文件包含多个注解,每一个成功被拦截到的注解都会触发generateForAnnotatedElement 方法的调用,屡次触发而获得的返回值,最终会写入到同一个文件当中。
Java-APT: 没法直接指定多个处理器之间的执行顺序
Dart-APT: 能够指定多个处理器之间的执行顺序,在配置文件build.yaml
中指定key值 runs_before
或 required_inputs
Java-APT: 注解处理器指定多个须要处理的注解后,能够在信息采集结束后统一处理
Dart-APT: 默认一个处理器只能处理一个注解,想要合并处理需指定处理器的执行顺序,先执行的注解处理器负责不一样类型注解的信息采集(采集的数据能够用静态变量保存),最后执行的处理器负责处理以前保存好的数据。
第三、第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中。
这解释了:
另一个文件example1.dart 则单独生成了文件 example1.g.dart。
这解释了:
*.dart
文件都会触发一次generate
方法调用,若是返回值不为空,则输出一个文件。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);
复制代码
library.annotatedWith
遍历的 Element 仅包括top-level级别的 Element,也就是那些文件级别的 Class、function等等,而Class 内部的 fields、functions并不在遍历范围,若是在 Class 内部的fields 或 functions 上修饰注解,GeneratorForAnnotation并不会拦截到!generateForAnnotatedElement
方法,也就是咱们自定义Generator所实现的抽象方法。generateForAnnotatedElement
返回值,也是咱们要生成的代码,调用normalizeGeneratorOutput
去作格式化。values
当中。值得再次说明的是: 以前咱们也提到过,当返回值不为空的状况下,每个文件输入源对应着一个文件输出。也就是说源码中,每个*.dart
文件都会触发一次generate
方法调用,而其中每个符合条件的目标注解使用,都会触发一次generateForAnnotatedElement
调用,若是被屡次调用,多个返回值最终会拼接起来,输出到一个文件当中。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);
}
}
}
复制代码
DartObject
对象,能够经过这个对象来取值,但为了便于使用,这里要将它再包装成API更友好的AnnotatedElement,而后返回。好啦~ 到这里你已经对 Dart-APT 有一个初步的认识了,应该具备使用 Dart-APT 的提升开发效率的能力了! APT 自己并不难,难的是利用 APT 的创意!期待你的想法与创做!
哦对了~ 全篇看下来,你应该会发现 Dart-APT 与 Java-APT 相比,它的实现仍是比较特殊的,对比 Java-APT,好多能力都暂不具有或实现起来比较繁琐,咱们整理下哦:
另外经过阅读 Generate 源码,咱们还意识到有一些能力 Dart-APT 能够实现但 Java-APT 很差实现:
Flutter仍是一个新兴技术, source_gen 目前只提供了最基础的APT能力,上面的这些功能的实现并非不能,而只是时间或ROI的问题了。
后面计划针对这些功能,产出一个 Dart-APT 扩展库,期待一下吧 (^__^)~