認識APT
APT(Annotation Process Tool),注解處理器,可以在編譯期或運行時獲取到注解信息,進行生成代碼源文件、其他文件或邏輯處理的功能。
Java中按注解保留的范圍可以分為三類,功能也各不相同,分別是:
-
SOURCE:編譯期間丟棄,編譯完成后這些注解沒有任何意義,可提供IDE語法檢查,靜態(tài)模版代碼例 :
@Override,@SuppressWarnings、Lombok -
CLASS: 保留在class文件中,類加載期間被丟棄,運行時不可見,可以用于字節(jié)碼操作、可獲取到加載類信息的動態(tài)代碼生成例:
AspectJ、ButterKnife、Room、EventBus3.0之后、ARouter -
RUNTIME:注解保留至運行期,結(jié)合反射技術(shù)使用例:
Retrofit、EventBus3.0之前
在應(yīng)用程序構(gòu)建的階段分布如圖:

第一階段為編譯期,由外部構(gòu)建工具將源代碼翻譯成目標可執(zhí)行文件,如exe。類似嵌入式c語言開發(fā)的構(gòu)建工具make、cmake,java中為javac。對應(yīng)SOURCE
第二階段為執(zhí)行期,生成的字節(jié)碼.class文件是JVM可執(zhí)行文件,由JVM加載.class文件、驗證、執(zhí)行的過程,在JVM內(nèi)部完成,把.class翻譯成平臺相關(guān)的本地機器碼。對應(yīng)CLASS
第三階段為運行時,硬件執(zhí)行機器碼過程,程序運行期間。對應(yīng)RUNTIME
Flutter出于安全性考慮,不支持反射,所以本文討論范圍不包含運行時部分功能
為什么使用代碼生成
在特定的場景下,代碼自動生成有很多好處,如下幾個場景:
- 數(shù)據(jù)類(Data classes):這些類型的類相當簡單,而且通常需要創(chuàng)建很多。因此,最好的方法是生成它們而不是手動編寫每一個
- 架構(gòu)樣板(Architecture boilerplate):幾乎每個架構(gòu)解決方案都會帶有一定數(shù)量的樣板代碼。每次重復(fù)編寫就會讓人很頭疼,所以,通過代碼生成可以很大程度上避免這種情況。 MobX就是一個很好的這樣的例子
-
公共特性/方法(Common features/functions):幾乎所有model類使用確定的方法,比如
fromMap,toMap,和copyWith。通過代碼可以一鍵生成所有這些方法
代碼生成不僅節(jié)省時間和精力,提高效率,更能提升代碼質(zhì)量,減少手動編寫的bug數(shù)量。你可以隨便打開任何生成的文件,并保證它能正常運行
項目現(xiàn)狀
使用領(lǐng)域驅(qū)動(DDD)架構(gòu)設(shè)計,核心業(yè)務(wù)邏輯層在domain層,數(shù)據(jù)獲取在service層,這兩層包含了穩(wěn)定數(shù)據(jù)獲取架構(gòu),提供了穩(wěn)定性的同時,也造成了項目架構(gòu)的弊病,包含大量的模版代碼。
經(jīng)過多次激烈討論,如果單純的將servce層刪掉,將勢必導(dǎo)致domain層耦合了數(shù)據(jù)層獲取的邏輯或是service層耦合底層數(shù)據(jù)池獲取的邏輯,對domain層只關(guān)心核心業(yè)務(wù)和將來數(shù)據(jù)池的擴展和遷移都造成不利影響,總之,每一層都有意義。所以,最終決定保留
不刪除又會導(dǎo)致,實現(xiàn)一個功能,要編寫很多代碼、類。為此需要一個開發(fā)中提升效率的折中方案
Dart運行時注解處理及代碼生成庫build剛好可以完成這個功能
確定范圍
確定好Flutter支持代碼生成的功能后,需要分析代碼結(jié)構(gòu)特點,確定使用范圍
分析代碼結(jié)構(gòu)
主要業(yè)務(wù)邏輯實現(xiàn)分為兩部分:
1、調(diào)用接口實現(xiàn)的獲取數(shù)據(jù)流程
2、調(diào)用物模型實現(xiàn)的屬性服務(wù)
兩部分都在代碼中有較高的書寫頻率,同時也是架構(gòu)樣板代碼的重災(zāi)區(qū),需要重點優(yōu)化
期望效果
定義好repo層,自動生成中間層代碼
文件名、類名遵循架構(gòu)規(guī)范
移動文件到指定位置

困難與挑戰(zhàn)
source_gen代碼生成配置流程、API熟悉、調(diào)試根據(jù)注解類信息,拿到類中方法,包括方法名、返回類型、必選參數(shù)、可選參數(shù)
物模型設(shè)置時,set/get方法調(diào)用不同API,返回參數(shù)為對象時,要添加convert方法自動轉(zhuǎn)換
接口生成類文件移動到指定目錄,物模型生成文件需要拼接
Build相關(guān)庫
類似java中的Java-APT,dart中也提供一系列注解生成代碼的工具,核心庫有如下幾個:
- build:提供代碼生成的底層基礎(chǔ)依賴庫,定義一些創(chuàng)建Builder的接口
- build_config:提供解析build.yaml文件的支持庫,由build_runner使用
- build_runner:提供了一些用于生成文件的通用命令,觸發(fā)builders執(zhí)行
- source_gen:提供build庫的上層封裝,方便開發(fā)者使用
生成器package配置
快速開始:
1、創(chuàng)建生成器package
創(chuàng)建注解解析器的package,配置依賴
dependency_overrides:
build: ^2.0.0
build_runner: ^2.0.0
source_gen: ^0.9.1
2、創(chuàng)建注解
創(chuàng)建一個類,添加const 構(gòu)造函數(shù),可選擇有參或無參:
class Multiplier {
final num value;
const Multiplier(this.value);
}
3、創(chuàng)建Generator
負責攔截解析創(chuàng)建的注解,創(chuàng)建類繼承GeneratorForAnnotation<T>,實現(xiàn)generate方法。和Java中的Processor類似
泛型參數(shù)是要攔截的注解,例:
class MultiplierGenerator extends GeneratorForAnnotation<Multiplier> {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
final numValue = annotation.read('value').literalValue as num;
return 'num ${element.name}Multiplied() => ${element.name} * $numValue;';
}
}
返回值是String,內(nèi)容就是生成的代碼,可以直接返回文本,例:
class PropertyProductGenerator extends Generator {
@override
String generate(LibraryReader library, BuildStep buildStep) {
final productNames = topLevelNumVariables(library)
.map((element) => element.name)
.join(' * ');
return '''
num allProduct() => $productNames;
''';
}
}
4、創(chuàng)建Builder
Generator是通過Builder觸發(fā)的,創(chuàng)建Builder
Builder metadataLibraryBuilder(BuilderOptions options) => LibraryBuilder(
MemberCountLibraryGenerator(),
generatedExtension: '.info.dart',
);
Builder multiplyBuilder(BuilderOptions options) =>
SharedPartBuilder([MultiplierGenerator()], 'multiply');
Builder 是build 庫中的抽象類
/// The basic builder class, used to build new files from existing ones.
abstract class Builder {
/// Generates the outputs for a given [BuildStep].
FutureOr<void> build(BuildStep buildStep);
Map<String, List<String>> get buildExtensions;
}
實現(xiàn)類在source_gen中,對Builder進行了封裝,提供更友好的API。執(zhí)行Builder要依賴build_runner ,允許通過dart 代碼生成文件,是編譯期依賴dev_dependency;只在開發(fā)環(huán)境使用
各個Builder作用:
-
PartBuilder:生成屬于文件的part of代碼。官方不推薦使用,更推薦SharedPartBuilder -
SharedPartBuilder:生成共享的可和其他Builder合并的part of文件。比PartBuilder優(yōu)勢是可合并多個部分文件到最終的一個.g.dart文件輸出 -
LibraryBuilder:生成單獨的Dart 庫文件 -
CombiningBuilder:合并其他SharedPartBuilder生產(chǎn)的文件。收集所有.*.g.part文件
需要注意的是SharedPartBuilder 會生成.g.dart后綴文件輸出,并且,執(zhí)行命令前,要在源文件引入
part '*.g.dart'才會生成文件LibraryBuilder,比較靈活,可以擴展任意后綴
5、配置build.yaml
創(chuàng)建的Builder要在build.yaml文件配置,build期間,會讀取該文件配置,拿到自定義的Builder
# Read about `build.yaml` at https://pub.dev/packages/build_config
builders:
# name of the builder
member_count:
# library URI containing the builder - maps to `lib/member_count_library_generator.dart`
import: "package:source_gen_example/builder.dart"
# Name of the function in the above library to call.
builder_factories: ["metadataLibraryBuilder"]
# The mapping from the source extension to the generated file extension
build_extensions: {".dart": [".info.dart"]}
# Will automatically run on any package that depends on it
auto_apply: dependents
# Generate the output directly into the package, not to a hidden cache dir
build_to: source
property_multiply:
import: "package:source_gen_example/builder.dart"
builder_factories: ["multiplyBuilder"]
build_extensions: {".dart": ["multiply.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
使用package配置
1、添加依賴
pubspec.yaml文件添加生成器package依賴。可添加到dev_dependencies
dev_dependencies:
source_gen_builder:
path: ../source_gen_builder
2、添加注解
在要生成文件類名添加注解,這里用官方例子
part 'library_source.g.dart';
@Multiplier(2)
const answer = 42;
const tau = pi * 2;
3、配置build.yaml
使用的package也需要配置build.yaml,用來定制化build行為。例如,配置注解掃描范圍,詳情見build_config
# Read about `build.yaml` at https://pub.dev/packages/build_config
targets:
$default:
builders:
# Configure the builder `pkg_name|builder_name`
# In this case, the member_count builder defined in `../example`
source_gen_builder|property_impl:
generate_for:
source_gen_builder|retrofit:
generate_for:
- lib/*/retrofit.dart
# The end-user of a builder which applies "source_gen|combining_builder"
# may configure the builder to ignore specific lints for their project
source_gen|combining_builder:
options:
ignore_for_file:
- lint_a
- lint_b
4、執(zhí)行命令
在使用的package根目錄下執(zhí)行:
flutter packages pub run build_runner build
結(jié)果展示:
生成*.g.dart文件
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: lint_a, lint_b
part of 'library_source.dart';
// **************************************************************************
// MultiplierGenerator
// **************************************************************************
num answerMultiplied() => answer * 2;
5、debug調(diào)試
復(fù)制該目錄下文件到使用package根目錄下

Android Studio下配置

點擊debug按鈕,打斷點調(diào)試即可
注意,debug需要生成器package和使用package在統(tǒng)一工程下才可以
配合腳本使用
上述生成文件都是帶.g.dart或其他后綴文件,并且目錄和源文件同級。如果想生成架構(gòu)中的模版源文件,并生成到其他目錄,可以配合腳本實現(xiàn),可以幫你完成:后綴名修改、移動文件目錄、文件代碼拼接的功能
這部分代碼根據(jù)個人情況實現(xiàn),大體框架如下
#!/bin/bash
# cd到執(zhí)行目錄
cd ../packages/domain
# 執(zhí)行build命令
flutter packages pub run build_runner build --delete-conflicting-outputs
# 循環(huán)遍歷目錄下文件,
function listFiles()
{
#1st param, the dir name
#2nd param, the aligning space
for file in `ls $1`;
do
if [ -d "$1/$file" ]; then
listFiles "$1/$file" "$2"
else
if [[ $2$file =~ "repository.usecase.dart" ]]
then
# 找到生成對應(yīng)后綴文件,執(zhí)行具體操作
# dosmothing
fi
if [[ $2$file =~ "repository.impl.dart" ]]
then
# dosmothing
fi
fi
done
}
listFiles $1 "."
總結(jié)
以上,就是利用Dart-APT編譯期生成代碼的步驟和調(diào)試過程
最后實現(xiàn)的效果可以做到只聲明業(yè)務(wù)層接口聲明,然后腳本一鍵生成service中間層實現(xiàn)。后面再有需求過來,再也不用費力梳理架構(gòu)實現(xiàn)邏輯和敲代碼敲的手指疼了
截止到目前,項目現(xiàn)在已有接口統(tǒng)計:GET 79、POST 97,并隨著業(yè)務(wù)持續(xù)增長。從統(tǒng)計編碼字符的維度來看,單個repo,一只接口,一個參數(shù)的情況下需手動編寫222個,自動生成1725個,效率提升88.6%
底層的數(shù)據(jù)獲取使用的retrofit,同樣是自動生成的代碼所以不計入統(tǒng)計字符范圍,這里的效率提升并不是指一個接口開發(fā)完成的整體效率,而是只涵蓋從領(lǐng)域到數(shù)據(jù)獲取中間層的代碼編寫效率
字符和行數(shù)優(yōu)化前后對比:

達到了既保證不破壞項目架構(gòu),又提升開發(fā)效率的目標