十九世紀中期一批不同凡響的猿猴誕生了,他們排斥重複的工做,畢生都在追求效率和性能。而用代碼去生成代碼,是這些猴子的一點小聰明。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 擴展庫,期待一下吧 (^__^)~