introduce

APT(Annotation Processing Tool) is a Tool for Processing annotations. Specifically, it is a javAC Tool used to scan and process annotations at compile time. The annotation processor takes Java code (or compiled bytecode) as input and generates **.java files as output. In simple terms,.java** files are generated by annotations at compile time.

role

Use APT advantage is convenient, simple, can reduce a lot of repeated code.

Those of you who have used annotation frameworks such as ButterKnife, Dagger, EventBus, etc., can see how much less code can be created with these frameworks, just by writing some annotations. In fact, they just generated some code through annotations. Through learning APT, you will find that they are very strong ~~~

implementation

With all that said, try it

The goal is to implement a function with APT that binds a View by annotating a View variable (similar to @bindView in ButterKnife)

(Reference here)

Create a project Create an Android Module name app Create a Java Library Module name apt-Annotation Create a Java Library Module name apt-Processor dependency Apt-annotation Creates an Android library Module named apt-Library that relies on apt-annotation and auto-service

Structure is as follows

The function is mainly divided into three parts

  • apt-annotation: custom annotation to store @bindView
  • apt-processor: Annotation processor, according toapt-annotationAnnotations in, generated at compile timexxxActivity_ViewBinding.javacode
  • apt-library: utility class, callxxxActivity_ViewBinding.javaIn the method, implementationViewThe binding.

Relations are as follows

The app? App is not a function code, just used to verify the function ~~~

1. Apt-annotation (custom annotations)

Create the annotation class BindView

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value(a);
}
Copy the code

@Retention(retentionPolicy.class) : indicates runtime annotation @target (elementtype.field) : indicates annotation scope is CLASS member (constructor, method, member variable)

RetentionPoicy.SOURCE, RetentionPoicy.CLASS, RetentionPoicy. Define the details of the modified object scope TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, etc

Here we define the runtime annotation BindView, where value() is used to get the ID of the corresponding View.

2, Apt-Processor (annotation processor)

(Key points)

Add dependencies to Module

dependencies {
    implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2' 
    implementation project(':apt-annotation')}Copy the code

When Android Studio was upgraded to 3.0, Gradle was also upgraded to 3.0. So we’re replacing compile with implementation

Create BindViewProcessor

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mMessager = processingEnv.getMessager();
        mElementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        // Generate Java files from annotations
        return false; }}Copy the code
  • init: Initializes. You can getProcessingEnviroment.ProcessingEnviromentProvides many useful utility classesElements.TypesFiler
  • getSupportedAnnotationTypes: specifies which annotation handler is registered forBindView
  • getSupportedSourceVersion: Specifies the Java version to use, usually returned hereSourceVersion.latestSupported()
  • process: This is where you write code that scans, evaluates, and processes annotations, generating Java files (The code in process is detailed below)
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
        mProxyMap.clear();
        // Get all the annotations
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new ClassCreatorProxy(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);
        }
        // Create a Java file by iterating through mProxyMap
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            try {
                mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
            }
        }

        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true; }}Copy the code

Through roundEnvironment. GetElementsAnnotatedWith (BindView. Class) get all annotations elements, and then save the information elements to mProxyMap, Finally, create the corresponding Java file through mProxyMap, where mProxyMap is the Map collection of ClassCreatorProxy.

ClassCreatorProxy is a proxy class that creates Java code as follows:

public class ClassCreatorProxy {
    private String mBindingClassName;
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
        this.mTypeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
        String packageName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packageName;
        this.mBindingClassName = className + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }

    /** * Create Java code *@return* /
    public String generateJavaCode(a) {
        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(mPackageName).append("; \n\n");
        builder.append("import com.example.gavin.apt_library.*; \n");
        builder.append('\n');
        builder.append("public class ").append(mBindingClassName);
        builder.append(" {\n");

        generateMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }

    /** * add Method *@param builder
     */
    private void generateMethods(StringBuilder builder) {
        builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append("host." + name).append("=");
            builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + ")); \n");
        }
        builder.append(" }\n");
    }

    public String getProxyClassFullName(a)
    {
        return mPackageName + "." + mBindingClassName;
    }

    public TypeElement getTypeElement(a)
    {
        returnmTypeElement; }}Copy the code

Elements, TypeElement, variable type, ID, etc., and StringBuilder to spell out the Java code bit by bit. Each object represents a corresponding. Java file.

Surprise! Java code can also be written like this ~~ take a look at the generated code in advance.

public class MainActivity_ViewBinding {
    public void bind(com.example.gavin.apttest.MainActivity host) {
        host.mButton = (android.widget.Button) (((android.app.Activity) host).findViewById(2131165218));
        host.mTextView = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165321)); }}Copy the code

Bugs Spelling Java code bit by bit through StringBuilder is tedious and easy to miswrite

A better solution would be to generate such Java code more easily with Javapoet. (More on that later)

1. Create a resources folder in the main directory of the Processors library. 2. Create META-INF/services directory in resources folder. 3, in the meta-inf/services directory folder to create javax.mail. Annotation. Processing. The Processor files; 4, the javax.mail. The annotation. Processing. Processor file is written to the full name of annotation Processor, including package path;) Is that too much trouble to write down? That’s why auto-service was introduced. AutoService annotation is automatically generated by @AutoService in auto-Service. The AutoService annotation processor was developed by Google. Used to generate the meta-inf/services/javax.mail annotation. Processing. The Processor file

3, apt-library utility class

We’ve finished the Processor part, and we’re almost done.

We created xxxactivity_viewbinding. Java in BindViewProcessor. Of course it’s reflection!!

Add dependencies to build.gradle in Module

dependencies {
    implementation project(':apt-annotation')}Copy the code

Create the annotation tool class BindViewTools

public class BindViewTools {

    public static void bind(Activity activity) {

        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch(InvocationTargetException e) { e.printStackTrace(); }}}Copy the code

The apt-Library part is relatively simple. Find the corresponding ViewBinding class by reflection, and then call the bind() method in it to complete the View binding.

So far, all the relevant code has been written, and it is finally ready to go

4, app

Dependencies in Module build.gradle (gradle >=2.2)

dependencies {
    implementation project(':apt-annotation')
    implementation project(':apt-library')
    annotationProcessor project(':apt-processor')
}
Copy the code

Android Gradle plugin version 2.2, Android Gradle plugin provides a function called annotationProcessor to completely replace Android -apt


(if Gradle<2.2) In Project build. Gradle:

buildscript {
    dependencies {
        classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'}}Copy the code

In Module buile.gradle:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    apt project(':apt-processor')}Copy the code

In MainActivity, add the BindView annotation in front of the View and pass in the ID

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView mTextView;
    @BindView(R.id.btn)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindViewTools.bind(this);
        mTextView.setText("bind TextView success");
        mButton.setText("bind Button success"); }}Copy the code

I think we all know the results of this operation, but to prove that the BindView function is complete, I will post the image

Generated code

The above functionality has been doing one thing, generating Java code, so where is the generated code? In the app/build/generated/source/apt can be found in the generated Java files

public class MainActivity_ViewBinding {
    public void bind(com.example.gavin.apttest.MainActivity host) {
        host.mButton = (android.widget.Button) (((android.app.Activity) host).findViewById(2131165218));
        host.mTextView = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165321)); }}Copy the code

Generate code from Javapoet

Above, in ClassCreatorProxy, StringBuilder is used to generate the corresponding Java code. This is cumbersome, but there is a more elegant approach, which is javapoet.

Add dependencies first

dependencies {
    implementation 'com. Squareup: javapoet: 1.10.0'
}
Copy the code

And then in ClassCreatorProxy

public class ClassCreatorProxy {
    // omit some code...

    /** * Create Java code *@return* /
    public TypeSpec generateJavaCode2(a) {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods2())
                .build();
        return bindingClass;

    }

    /** * add Method */
    private MethodSpec generateMethods2(a) {
        ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(host, "host");

        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            methodBuilder.addCode("host." + name + "=" + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
        }
        return methodBuilder.build();
    }


    public String getPackageName(a) {
        returnmPackageName; }}Copy the code

And then finally in the BindViewProcessor

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // omit some code...
        // Generated by Javapoet
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode2()).build();
            try {
                //  Generate the file
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }
Copy the code

It’s much cleaner and much simpler than using StringBuilder to spell out Java code. The resulting code is the same as before, so I won’t post it.

Javapoet usage details

The source code

GitHub

reference

Compile time annotations (APT) this is just the beginning of the compilation time annotation parsing framework You must know APT, annotationProcessor, Android-apt, Provided, custom annotations

Thank you for pointing out the above mistakes