In the mid-19th century a different breed of ape was born, one that rejected repetitive work and devoted its life to efficiency and performance. And code to generate code, is a little bit clever monkeys.
The monkey said, “A family should be neat!” So even the nascent Flutter was endowed with this ability by monkeys.
This article will start with a simple demo to give you a first look at the annotation handling and code generation of Flutter, which is actually Dart.
We’ll then explain the various aspects of annotation processing and the Api in detail to help you get rid of all the confusion and learn how to use Dart annotation processing.
To simplify the description, Dart annotation processing is referred to as DART-APT.
After that, we will make a comparison between Java-APT and Dart-APT. On the one hand, we will strengthen your cognition, and on the other hand, we will introduce several special points of Dart-APT.
Finally, we will briefly analyze the source code of darT-APT Generator to help you better understand and use dart-APT.
Outline of this paper:
- 1. Meet the Dart – APT
- 2. The Dart – APT Api, rounding
- 3. Comparison between Java-APT and Dart-APT and the particularity of Dart-APT
- 4. Dart-apt Generator source analysis
Introduction to Dart annotation processing and code generation
In the first section, I will give you a quick overview of Flutter annotation processing and code generation using a simple demo. The specific API details of Flutter will be explained later.
The annotation processing of Flutter, which is actually Dart, depends on source_gen. The details can be found on its Github homepage. We won’t expand too much here, just know [dart-apt Powered by source_gen]
Applying annotations and generating code in Flutter requires only a few steps:
- 1. Rely on source_gen
- 2. Create annotations
- 3. Create a generator
- 4. Create a Builder
- 5. Write a configuration file
1. Rely onsource_gen
The first step is to introduce source_gen in your project’s pubspec.yaml. If you only use this code locally and don’t plan to publish it as a library:
dev_dependencies:
source_gen:
Copy the code
Otherwise,
dependencies:
source_gen:
Copy the code
Create annotations and use them
Dart annotation creation is much more naive than Java annotation creation, with no extra keywords, and is really just a constructor that needs to be decorated with const plain classes.
For example, declare an annotation with no arguments:
class TestMetadata {
const TestMetadata();
}
Copy the code
Use:
@TestMetadata()
class TestModel {}
Copy the code
Declare a comment with parameters:
class ParamMetadata {
final String name;
final int id;
const ParamMetadata(this.name, this.id);
}
Copy the code
Use:
@ParamMetadata("test", 1)
class TestModel {}
Copy the code
3. Create a generator
Similar to the Java-APT Processor, in Dart’s world, the Generator has the same responsibilities.
You need to create a Generator, inheritance in GeneratorForAnnotation, and realize: generateForAnnotatedElement method.
Also fill in the GeneratorForAnnotation’s generic parameter with the annotations we’re intercepting.
class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "class Tessss{}"; }}Copy the code
The return value is a String whose content is the code you will generate.
You can be acquired by using three parameters of generateForAnnotatedElement method annotations of all kinds of information, used to generate the corresponding code. The specific use of the three parameters will be discussed later.
Here we simply return a string “class Tessss{}” to see the effect.
4. Create a Builder
The execution of the Generator needs to be triggered by the Builder, so now we will create a Builder.
It’s as simple as creating a global method that returns type Builder:
Builder testBuilder(BuilderOptions options) =>
LibraryBuilder(TestGenerator());
Copy the code
The name of the method is arbitrary, but it is the object that is returned that matters.
In our example we return the LibraryBuilder object, and the constructor takes the TestGenerator object we created in the previous step.
There are actually other Builder objects to choose from, depending on your needs:
- Builder
- _Builder
- PartBuilder
- LibraryBuilder
- SharedPartBuilder
- MultiplexingBuilder
- _Builder
PartBuilder and Share PartBuilder involve the use of the Dart-Part keyword. We won’t expand this for now, but LibraryBuilder is usually sufficient for our needs. MultiplexingBuilder supports the addition of multiple Builders.
5. Create a configuration file
The build.yaml file is created in the project root directory to configure the parameters of the Builder:
builders:
testBuilder:
import: "package:flutter_annotation/test.dart"
builder_factories: ["testBuilder"]
build_extensions: {".dart": [".g.part"]}
auto_apply: root_package
build_to: source
Copy the code
Details about the configuration information are explained later. The important thing is that we specify the Builder we created in the previous step with the import and builder_factories tags.
6. Run the Builder
Execute the command from the command line to run our Builder
$ flutter packages pub run build_runner build
Copy the code
Due to Flutter’s prohibition of reflection, you can no longer use compile-time annotations as Android does. The coding phase uses interfaces, the compilation phase generates implementation classes, and the runtime phase creates implementation class objects through reflection. In Flutter, you can only generate code by command first and then directly use the generated code.
As you can see the command is still too long, one possible suggestion is to encapsulate the command as a script.
If the command is successfully executed, a new file will be generated: testModel.g.art
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// TestGenerator
// **************************************************************************
class Tessss {}
Copy the code
Code generation successful!
To clear the generated files, run the following command:
flutter packages pub run build_runner clean
Copy the code
The Dart – APT Api, rounding
- 1. Annotation creation and use
- 2. Create a Generator
- 3. GenerateForAnnotatedElement parameters: element
- 4. GenerateForAnnotatedElement parameters: the annotation
- 5. GenerateForAnnotatedElement parameters: buildStep
- 6. Template code generation techniques
- 7. Parameter Meaning of the configuration file
1. Annotation creation and use
Dart annotation creation is no different from normal class creation. It can extends, implements, or even with.
The only requirement is that the constructor be decorated with const.
Unlike Java annotations, which are created by specifying @target (which defines the range of objects that can be modified)
Dart annotations have no scope to modify. Defined annotations can modify classes, properties, methods, and parameters.
It is important to note that if your Generator directly inherits from GeneratorForAnnotation, your Generator will only intercept top-level elements, not internal class properties, methods, etc. Class internal attribute, method modifier annotations are currently meaningless. (But this thing can certainly be extended to achieve ~)
2. Create a Generator
The Generator is created for creating code. Typically, we will inherit GeneratorForAnnotation and add the target annotation to its generic parameter. Then autotype generateForAnnotatedElement method, and ultimately return a string, we come to the generated code.
class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
return "class Tessss{}"; }}Copy the code
GeneratorForAnnotation notes are:
2.1 Mapping between GeneratorForAnnotation and annotation
GeneratorForAnnotation is a single-annotation processor, and each GeneratorForAnnotation must have one and only one annotation as its generic parameter. That is, each generator that inherits from GeneratorForAnnotation can only handle one annotation.
2.2 generateForAnnotatedElement parameters meaning
Is one of the most notable generateForAnnotatedElement method of three parameters: Element Element, ConstantReader annotation, BuildStep BuildStep. The information we rely on to generate our code comes from these three parameters.
- Element Element: An Element that is decorated by an annotation, using which an Element’s name, visibility, and so on can be obtained.
- ConstantReader annotation: Represents an annotation object that can be used to obtain annotation information and parameter values.
- BuildStep BuildStep: The build information from which to get input and output information, such as input filename, etc
GenerateForAnnotatedElement the return value is a String, you need to give you want the generated code with String concatenation, return null means don’t need to generate the file.
2.3 Code and file generation rules
Unlike Java APT, file generation is completely developer custom. The GeneratorForAnnotation file generates its own set of rules.
In depth do other custom case, if generateForAnnotatedElement return values Will never be empty, is:
-
If a source file contains only one class that is modified by the target annotation, then there is a generate file for each file that contains the target annotation.
-
If a source file contains multiple is decorated target annotation class, generate a file, generateForAnnotatedElement method is executed repeatedly, the code generated by two newline after joining together, the output to the file.
3. GenerateForAnnotatedElement parameters: Element
For example, we have code that uses the @testmetadata annotation:
@ParamMetadata("ParamMetadata", 2)
@TestMetadata("papapa")
class TestModel {
int age;
int bookNum;
void fun1() {}
void fun2(int a) {}
}
Copy the code
In generateForAnnotatedElement method, we can through the Element parameters for TestModel some simple information:
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
...
Copy the code
GeneratorForAnnotation is a class only Field, and only the class information of TestModel can be obtained from element. How to obtain the Field and method information of TestModel?
Kind: CLASS. Kind identifies the type of an element. It can be CLASS, FIELD, FUNCTION, etc.
For these types, there are subclasses of Element: ClassElement, FieldElement, FunctionElement, etc., so you can do this:
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 fun2(int a) → void fun2(int aCopy the code
4. GenerateForAnnotatedElement parameters: the annotation
In addition to marking annotations, carrying parameters is one of the most important capabilities of annotations. The parameters carried by an annotation can be obtained using the annotation:
annotation.runtimeType: _DartObjectConstant
annotation.read("name"): ParamMetadata
annotation.read("id"): 2
annotation.objectValue: ParamMetadata (id = int (2); name = String ('ParamMetadata'))
Copy the code
The annotation type is ConstantReader. In addition to providing a read method to get specific parameters, the annotation also provides a peek method. The two methods have the same capabilities, but the difference is that if the read method reads a nonexistent parameter name, it will throw an exception. Instead, null is returned.
5. GenerateForAnnotatedElement parameters: buildStep
BuildStep provides the inputs and outputs for this build:
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
Copy the code
6. Template code generation techniques
Now that you have the three input sources available, the next step is to generate code from that information.
How do you generate the code? You have two options:
6.1 Simple template code, string concatenation:
If the code you need to generate is not very complex, you can concatenate strings directly, for example:
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();
}
Copy the code
In general, we don’t recommend doing this because it’s error-prone and unreadable.
6.2 Complex template code, DART multi-line string + placeholder
Dart provides a triple-quoted syntax for multi-line strings:
var str3 = """Your Majesty asked me to come here and meet the Tathagata.""";
Copy the code
Combined with placeholders, we can achieve cleaner template code:
tempCode(String className) {
return """
class ${className}APT {
}
""";
}
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
...
return tempCode(element.name);
}
Copy the code
If there are too many arguments, the arguments to the tempCode method can be replaced with a Map.
(Do not forget to import package in template code. It is recommended to write template code in the compiler first, the compiler static check is ok, then put in triple quotes to modify the placeholder)
If you’re familiar with Java-APT, you might be wondering: Is there a javapoet code base in Dart to help generate code? Personally, I recommend the second approach to code generation because it is clear and readable enough to make it easier to understand and write template code than javapoet.
7. Parameter Meaning of the configuration file
Create a build.yaml file in the project root directory to configure Builder information.
Take the following configuration as an example:
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
Copy the code
Configure all your Builders under Builders. Test_builder and test_Builder2 are both your Builder names.
- The import keyword is used to import the package of return Builder methods (mandatory)
- Builder_factories fills in the method name of our return Builder (must)
- Build_extensions specifies the mapping from the input extension to the output extension, such as we accept
.dart
File input, final output.g.dart
Documents (required) - Auto_apply specifies that builder applies to, optional: (Optional, default none)
- “None” : Do not apply this Builder unless manually configured
- “Dependents” : Apply this Builder to a package that directly depends on the package that exposes the Builder.
- “All_packages” : Apply this Builder to all packages in the pass dependency diagram.
- “Root_package” : Apply this Builder only to top-level packages.
- Build_to specifies the output location. Optional value: (optional, default is cache)
- “Source “: output to the source tree of its main input
- “Cache “: output to the hidden build cache
- Required_inputs Specifies one or a series of file extensions to run after any Builder that may produce this type of output (optional)
- Runs_before is guaranteed to run before the specified Builder
Configuration fields are a bit tricky to explain, but I’ve only listed some of the most common ones here, and some of the less common ones can be found on source_gen’s Github page.
The comparison between Java-APT and Dart-APT and the particularity of Dart-APT
Here we will list the main differences between Java-APT and Dart-APT, compare and contrast them to deepen your understanding and provide considerations.
1. Annotation definition
Java-apt: Specify when annotations are defined (encoding phase, source phase, runtime phase) and annotation scope (class, method, attribute)
Dart-apt: No need to specify annotation parsing time and annotation scope, default Anytime and anywhere
2. The relationship between annotations and annotation handlers
Java-apt: An annotation processor can specify multiple annotations for processing
Dart-apt: Uses the default handler provided by source_gen: GeneratorForAnnotation, each handler can only handle one annotation.
3. Annotation interception scope
Java-apt: Every annotation that is legally used can be intercepted by the annotation handler.
Dart-apt: uses the default processor provided by source_gen: GeneratorForAnnotation. The processor can only handle top-level elements, such as classes, functions, enums, and so on, defined directly in the.dart file. However, annotations used on Fields and functions within a class cannot be intercepted.
4. Relationship between annotations and build files
Java-apt: Annotations are not directly related to the number of generated files
Dart-apt: In the case that the annotation handler does not return a null value, there is usually one output file for each input file. If you do not want to Generate a file, just return null in the Generate method. If an input file contains multiple annotations, every successful intercepted to annotation will trigger generateForAnnotatedElement method calls, many times the return value of the trigger, will eventually be written to the same file.
5. Annotate the running order between processors
Java-apt: You cannot directly specify the order of execution between multiple processors
Dart-apt: You can specify the execution order between multiple processors by specifying the key value runs_before or required_inputs in the build.yaml configuration file
6. Merge multiple annotation information
Java-apt: Annotation processor specifies multiple annotations to be processed, which can be processed uniformly after information collection
Dart-apt: By default, each annotation processor can only process one annotation. To combine the annotation processing, you need to specify the processor execution order. The annotation processor that executes first is responsible for collecting information about different annotation types (the collected data can be stored in static variables), and the processor that executes last is responsible for processing previously saved data.
Points 3 and 4 are very different from Java-APT and you may still be a little confused. Here is a chestnut to illustrate:
chestnuts
Suppose we have two files:
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() {}}Copy the code
example1.dart
@ParamMetadata("ClassThree", 5)
class Three {
int age;
void fun1() {}}Copy the code
Generate is implemented as follows:
class TestGenerator extends GeneratorForAnnotation<ParamMetadata> {
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
print("Current input source:${buildStep.inputId.toString()}Intercepted elements:${element.name}Annotation values:${annotation.read("name").stringValue} ${annotation.read("id").intValue}");
return tempCode(element.name);
}
tempCode(String className) {
return """
class ${className}APT {
}
"""; }}Copy the code
Execute flutter Packages pub run build_runner build
Console output:
The current input source: flutter_annotation | lib/example. The dart is intercepted element: One note value: ClassOne 1 current input sources: Flutter_annotation | lib/example. The dart is intercepted elements: Two annotations value: ClassTwo 4 current input source: flutter_annotation | lib/example1. The dart is intercepted element: ClassThree 5Copy the code
Generated files:
- lib
- example.dart
- example.g.dart
- example.dart
- example1.g.dart
Copy the code
example.g.dart
class OneAPT {}
class TwoAPT {}
Copy the code
example1.g.dart
class ThreeAPT {}
Copy the code
Chestnut summary
Dart we have two classes annotated, one of which has fields and functions annotated in addition to the Class itself.
But in the output, we only intercepted ClassOne, not Field1 fun1.
This explains:
library.annotatedWith
The traversal of elements includes only top-level elements (file-level classes, functions, etc.). The traversal of fields, functions, etc. GeneratorForAnnotation does not intercept annotations if they are modified on fields or functions inside a Class!
Dart: the generated.g.art file contains both Class One and Class Two in example.dart, so the generated code is spliced together in example.g.art.
This explains:
- If an input file contains multiple annotations, every successful intercepted to annotation will trigger generateForAnnotatedElement method calls, many times the return value of the trigger, will eventually be written to the same file.
Dart separately generates the file example1.g.dart.
This explains:
- Each file input source corresponds to one file output when the return value is not null. That is, in the source code, every single one of them
*.dart
Files are triggered oncegenerate
Method call, outputs a file if the return value is not null.
Dart-APT Generator
1. Brief analysis of Generator source code
Stir-fry chicken simple:
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();
}
Copy the code
With just a few lines of code, when the Builder runs, it calls the Generator’s generate method with two important arguments:
library
From this, we can obtain source code information as well as annotation informationbuildStep
It represents a step in the build process that allows us to get input and output information for some files
It is worth noting that the library contains the source information for individual elements, which can be classes, functions, enums, and so on.
In source_gen, GeneratorForAnnotation is the only subclass of Generator.
abstract class GeneratorForAnnotation<T> extends Generator { const GeneratorForAnnotation(); / / 1typeChecker is used for annotation checking TypeChecker GETtypeChecker => TypeChecker.fromRuntime(T); @override FutureOr<String> generate(LibraryReader library, BuildStep buildStep) async { var values = Set<String>(); //2 Iterate over all elements that meet the annotation type criteriafor (var annotatedElement in library.annotatedWith(typeThe Checker)) {/ / 3 inspection conditions of call generateForAnnotatedElement perform developers custom code generation logic var generatedValue = generateForAnnotatedElement ( annotatedElement.element, annotatedElement.annotation, buildStep); //4 generatedValue is the code string to be generated, formatted await with normalizeGeneratorOutputfor (var value innormalizeGeneratorOutput(generatedValue)) { assert(value == null || (value.length == value.trim().length)); // add the generated code to the collection values.add(value); }} / / 6return values.join('\n\n');
}
//7
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep);
Copy the code
- //1: typeChecker is used for annotation checks to see if the target annotation is modified on the Element
- //2: library.annotatedWith(typeChecker) iterates over all elements and checks if they annotate the target annotation with typeChecker. Again, it is worth stating: Library.annotatedwith traverses only include top-level elements, meaning file-level classes, functions, and so on, Fields and functions inside a Class are not in traversal scope. GeneratorForAnnotation does not intercept annotations if they are modified on fields or functions inside a Class!
- //3: call when conditions are met
generateForAnnotatedElement
Method, which is the abstract method that we implement with our custom Generator. - / / 4: generatedValue is
generateForAnnotatedElement
The return value, which is the code we’re going to generate, is callednormalizeGeneratorOutput
Go do the formatting. - //5: Add to collection when conditions are met
values
In the middle.It’s worth noting again that, as we mentioned earlier, each file input source corresponds to one file output when the return value is not null. That is, in the source code, every single one of them*.dart
Files are triggered oncegenerate
Method call, which fires once for each qualifying target annotation usegenerateForAnnotatedElement
Call, if called multiple times, multiple return values will eventually be concatenated and output to a file. - //6: Each individual output is separated by two newlines, and finally output to a file.
- //7: We customize the abstract methods implemented by the Generator.
Library. AnnotatedWith source code analysis
The GeneratorForAnnotation source code is also very simple, the only notable is the library. AnnotatedWith method, let’s look at the source code:
class LibraryReader { final LibraryElement element; //1 element input source LibraryReader(this.element); . //2 allElements, but only 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 inAllElements) {/ / 3 if modify the multiple same annotations, will only take the first final annotations. = the checker firstAnnotationOf (element, throwOnUnresolved: throwOnUnresolved);if(annotation ! = null) {//4 Wrap annotation as AnnotatedElement object returns yield AnnotatedElement(ConstantReader(annotation), element); }}}Copy the code
- //1: The Element object is a standard composite pattern, which is easy to misunderstand: this Element is the root Element of all source code in the applied project. This is not true. The correct answer is: The scope of this Element and its children is limited to one file.
- //2: allElements are limited to top-level sub-elements only
- //3: This checker checks the annotations decorated by Element. If more than one of the same annotations is decorated, only the first one is taken. If there is no target annotation, null is returned
- //4: The returned annotation is actually just one
DartObject
Object, which can be evaluated by this object, but for ease of use, it’s going to be rewrapped as the apI-friendly AnnotatedElement, and then returned.
conclusion
Now that you have a basic understanding of DARt-APT, you should be able to use dart-APT to improve development efficiency. APT itself is not difficult, difficult is the use of APT creativity! Looking forward to your ideas and creations!
Dart-apt is a java-apt, but it’s not a java-APT. It’s a java-APT, but it’s a java-APT, and it’s a java-APT.
- You cannot intercept annotations that are used inside a class for attributes, methods, and so on
- An annotation handler can only handle one annotation
- No direct API custom file generation, etc
- It is complicated to merge multiple annotation information
In addition, by reading Generate source code, we also realized that there are some capabilities that dart-APT can implement but java-apt is not easy to implement:
- Intercepts a Class or all subclasses derived from that Class directly, without annotations.
Flutter is still a new technology and Source_gen currently provides only the most basic APT capabilities. It is not impossible to implement these capabilities, but only a matter of time or ROI.
A darT-APT extension library is planned for these features. Expect to see (^__^)~