第一步,在你工程的 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 {
@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 {
@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(“KaTeX parse error: Undefined control sequence: \n at position 3: e \̲n̲"); } for (var …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_before
或 required_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 {
@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 “”"
尾声
面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Android核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、热修复设计、插件化框架解读、组件化框架设计、图片加载框架、网络、设计模式、设计思想与代码质量优化、程序性能优化、开发效率优化、设计模式、负载均衡、算法、数据结构、高级UI晋升、Framework内核解析、Android组件内核等。
不仅有学习文档,视频+笔记提高学习效率,还能稳固你的知识,形成良好的系统的知识体系。这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
Android进阶学习资料库
一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
大厂面试真题
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
《2017-2021字节跳动Android面试历年真题解析》
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
[外链图片转存中…(img-YGpHmqOX-1714961255972)]
大厂面试真题
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-UvW880di-1714961255972)]
《2017-2021字节跳动Android面试历年真题解析》
[外链图片转存中…(img-qaNLdGXI-1714961255973)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!