Android APT technology was mentioned in the previous “Realizing Android Componentization by Pure Handwritten Routing Framework”, and APT technology was used in the explanation video. JavaPoet assisted code generation implements the simplest annotation-based View injection (which omits a lot of the findViewById methods). If you just want to experience the convenience brought by APT technology, then this article is very suitable. APT is a compile-time Annotation Processing Tool. Some of the leading tripartite libraries, such as ButterKnife and EventBus, use this technique to generate code.

The following is reprinted from Android APT (Compile-time Code Generation) Best Practices

APT

APT(Annotation Processing Tool) is a Tool for Processing annotations. It detects the annotations in source code files and uses annotations to perform additional Processing. When processing annotations, the Annotation processor can generate additional source files and other files according to the annotations in source files (the specific content of files is determined by the Annotation processor writer). APT will also compile the generated source files and original source files. Put them together to generate a class file.

APT HelloWorld

To create an Annotation Module

First, we need to create a New Java Library called Annotations, which will place annotations and associated code that will be used in the project. Here is a simple custom annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS) 
public @interface Test {   }
Copy the code

Build. Gradle is configured to specify the JDK version

Utf-8 tasks.withType(JavaCompile){options.encoding = "utF-8"} Java {plugins {id 'java-library'} sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }Copy the code

Create a Compiler Module

Create a Java Library called Compiler that will write the associated code generated by the code. The core is here, configure build.gradle:

plugins { id 'java-library' } dependencies { implementation fileTree(dir: 'libs', includes: [' *. Jar ']) / / compile time annotation processing annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 rc4' compileOnly 'com. Google. Auto. Services: auto - service: 1.0 rc4' / / help us to generate Java code by means of class call [JavaPoet] implementation 'com. Squareup: javapoet: 1.10.0 / / is dependent on the annotation implementation project (' : the annotation')} / / console Chinese set utf-8 tasks.withType(JavaCompile){ options.encoding = "UTF-8" } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }Copy the code

1. Set the JDK version to 1.8. This is very important. 2. The main function of AutoService is to annotate the Processor class and generate meta-INF configuration information for it. 3. JavaPoet is a library that helps us generate code in the form of class calls. 4. Rely on the Annotation Module created above.

Define Processor class

This is where the logic for generating the code is.

@AutoService(Processor.class) public class TestProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(Test.class.getCanonicalName()); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false; }}Copy the code

To generate the first class, we’ll generate the following code for HelloWorld:

package com.example.helloworld;
public final class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, JavaPoet!");
    }
}
Copy the code

Modify the TestProcessor process method above

@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!" ) .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } return false; }Copy the code

Use it in the app

Configure build.gradle for the app module

dependencies {

    ......

    implementation project(':annotation')
    annotationProcessor project(':annotation-processor')
}
Copy the code

Add the @test annotation to an arbitrary class, such as in MainActivity:

@Test
public class MainActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	}
}
Copy the code

Click the ReBuild Project of Android Studio, and you can see the generated code in the build/generated/ AP_generated_sources /debug/out directory of the app.

Annotation-based View injection: DIActivity

So far we haven’t used annotations and @test above is not actually used, so let’s do some more practical code generation. Implement annotation-based View instead of findByView in the project. This is just to learn how to use APT. If you really want to use DI framework, ButterKnife is recommended.

The first step is to create @DiActivity and @DiView annotations in the Annotation Module.

package cn.tim.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {

}
Copy the code
package cn.tim.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DIView {
    int value() default 0;
}
Copy the code

Create the DIProcessor method

package cn.tim.annotation_processor; import com.google.auto.service.AutoService; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import cn.tim.annotation.DIActivity; import cn.tim.annotation.DIView; @AutoService(Processor.class) public class DIProcessor extends AbstractProcessor { private Elements elementUtils; @ Override public Set < String > getSupportedAnnotationTypes () {/ / annotations need to deal with the return Collections.singleton(DIActivity.class.getCanonicalName()); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { System.out.println("DIProcessor"); Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DIActivity.class); For (Element: elements) {Class TypeElement TypeElement = (TypeElement) Element; List<? extends Element> members = elementUtils.getAllMembers(typeElement); MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.VOID) .addParameter(ClassName.get(typeElement.asType()), "activity"); for (Element item : members) { DIView diView = item.getAnnotation(DIView.class); if (diView == null){ continue; } bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value())); } TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName()) .superclass(TypeName.get(typeElement.asType())) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(bindViewMethodSpecBuilder.build()) .build(); JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } return true; } private String getPackageName(TypeElement type) { return elementUtils.getPackageOf(type).getQualifiedName().toString(); } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_8; }}Copy the code

Using DIActivity

package cn.tim.apt_demo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import cn.tim.annotation.DIActivity;
import cn.tim.annotation.DIView;

@DIActivity
public class MainActivity extends AppCompatActivity {

    @DIView(value = R.id.text)
    TextView textView;

    @DIView(value = R.id.text1)
    TextView textView1;

    @DIView(value = R.id.text2)
    TextView textView2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DIMainActivity.bindView(this);
        textView.setText("Hello, JavaPoet!");

        textView2.setText("Tim");
    }
}
Copy the code

Apt generates the following code:

public final class DIMainActivity extends MainActivity { public static void bindView(MainActivity activity) { activity.textView = (android.widget.TextView) activity.findViewById(2131231086); activity.textView1 = (android.widget.TextView) activity.findViewById(2131231087); activity.textView2 = (android.widget.TextView) activity.findViewById(2131231088); }}Copy the code

Example code: Github -> aptdemo