Flutter 註解處理及程式碼生成

暴打小女孩發表於2019-07-02

十九世紀中期一批與眾不同的猿猴誕生了,他們排斥重複的工作,畢生都在追求效率和效能。而用程式碼去生成程式碼,是這些猴子的一點小聰明。

猴子說:“一家人就要整整齊齊!” 所以即使是新興的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 擴充套件庫,期待一下吧 (^__^)~

相關文章