十九世紀中期一批與眾不同的猿猴誕生了,他們排斥重複的工作,畢生都在追求效率和效能。而用程式碼去生成程式碼,是這些猴子的一點小聰明。
猴子說:“一家人就要整整齊齊!” 所以即使是新興的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
- _Builder
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_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<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 擴充套件庫,期待一下吧 (^__^)~