“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

Today we first on the demo code, and then in APT technology to introduce.

What is the APT

APT(Annotation Processing Tool) is literally Annotation processor, should be related to annotations, Annotation Processing Tool. By scanning and processing annotations at compile time. The annotation processor then generates Java code from the annotations. This is simply a.java file generated by annotations at compile time. Do you want to think about metaprogramming?

How to use APT in a project

The goal of the Demo

The goal is to see how APT helps us automatically generate code at compile time through its own view binding feature. Collect some information online and do it yourself. It sounds like a Butter Knife pair that implements something like Butter Knife.

View binding has recently been implemented using Binding. I’ve moved a bit away from ButterKnife, but I still have fond memories of the library and its clean and concise API.

Create a project

Create an empty Android project, and then add modules to the project. Each module implements different functions in APT, so that is to distinguish modules by functions.

Create a module

  • Note that this is a (Java module) in which you can create the ZiViewBind annotation

  • Create the ZI-API (Android module) to provide the user can call the API, which implements the view binding.

  • Zi-compiler (Java module) is used to write annotation processing, and zi-API, which is an Android model, is a compilation module type that you need to specify, and then a Java module called Zi-Annotation, Used in this module to create annotations
No. Module name Module type instructions
1 zi-annotation Java module In this module, you can create annotations
2 zi-api Android module Provides an API that users can call
3 zi-compiler Java module Write annotation processing

Android Studio automatically introduces the created modules in the Setting file when we create them.

rootProject.name='zi application'
include ':app'
include ':zi-annotation'
include ':zi-api'
include ':zi-compiler'
Copy the code

Dealing with dependencies

These modules are also dependent on each other. First, configure them in build.gradle(ZI-Compiler) file as follows

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs'.include: ['*.jar'])
    implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2'
    implementation 'com. Squareup: javapoet: 1.10.0'
    implementation project(':zi-annotation')
}

sourceCompatibility = "Seven"
targetCompatibility = "Seven"

Copy the code
  • Pay attention tozi-compilerDepends on thezi-annotationThe module
Implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2' implementation 'com. Squareup: javapoet: 1.10.0'Copy the code

Tip: Add the newly created modules to the main project, and note that the ZI-Compiler is annotationProcesso because the main job of the ZI-Compiler is to process annotations to generate Java code at compile time

dependencies {
    ...

    implementation project(':zi-annotation')
    implementation project(':zi-api')
    annotationProcessor project(':zi-compiler')}Copy the code

Now that the modules are created, added, and dependent on each other, all that remains is to add code to those modules. Here we are talking about the Android project construction is still quite tedious, fortunately Android Studio such a tool to help us complete a lot of work, and give us a certain guide to build Android.

Zi – the annotation module

Create the ZiBindView annotation
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface ZiBindView {
}
Copy the code

@Target specifies the range of objects that the Annotation is modifying

value instructions
CONSTRUCTOR Used to describe a constructor
FIELD Used to describe domains
LOCAL_VARIABLE Used to describe local variables
METHOD Used to describe methods
PACKAGE Describe packages
PARAMETER Describe parameters
TYPE Used to describe a class, interface (including annotation type), or enum declaration

Zi – compiler module

Annotations that create ZiBindViewProcessor and ClassCreatorProxy are not covered in the code are not explained here

@Retention defines how long the Annotation is retained: some annotations only appear in source code and are discarded by the compiler; Others are compiled in class files; Annotations compiled in a class file may be ignored by the virtual machine, while others will be read when the class is loaded (note that class execution is not affected, since annotations and classes are used separately). Using this meta-annotation, you can limit the “life cycle” of an Annotation.

Annotation processors need to inherit from AbstractProcessor, where some code writing is basically fixed.

public class ZiBindViewProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false; }}Copy the code

GetSupportedSourceVersion getSupportedAnnotationTypes () returns the annotation of support types () returns the source version of support

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        // Get the class name
        supportTypes.add(ZiBindView.class.getCanonicalName());
        return supportTypes;
    }
Copy the code

Here, getName() returns a representation of the class in the virtual machine, while getCanonicalName() returns a more understandable representation. There is no difference between the two methods for most classes. But there is a difference for array or inner classes. In addition, class loading (virtual machine loading) requires the class name to be getName.

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return SourceVersion.latestSupported();
    }
Copy the code
To collect information

“Information collection” means that according to our declaration, we get the corresponding Element, and then annotate to collect the required information, which is used for the later production of objects

Production proxy class (A production proxy class that produces text into classes at compile time, ClassCreatorProxy

An annotation class is produced for each

Initialize the
public interface ProcessingEnvironment {
    Elements getElementUtils(a);
    Types getTypeUtils(a);
    Filer getFiler(a);
    Locale getLocale(a);
    Messager getMessager(a);
    Map<String, String> getOptions(a);
    SourceVersion getSourceVersion(a);
    
}
Copy the code

ZiBindViewProcessor complete code

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"zidea.example.com.zi_compiler.ZiBindViewProcessor"})
@AutoService(Processor.class)
public class ZiBindViewProcessor extends AbstractProcessor {

    // Log related helper classes
    private Messager mMessager;
    // An element helper class to help us get information about the element.
    private Elements mElementUtils;
    private Map<String,ClassCreatorProxy> mProxyMap = new HashMap<>();

    // Get Elements using the ProcessingEnvironment object
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }

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

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

    // Collect information
    // The so-called information collection means that we get the corresponding Element according to our declaration, and then annotate it to collect the information needed for later production of objects
    // Production proxy class (At compile time, the production proxy class ClassCreatorProxy, which produces text into classes, produces an annotation class for each
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // Output information
        mMessager.printMessage(Diagnostic.Kind.NOTE,"processing...");

        // Follow the annotations to get the required information

        GetElementsAnnotatedWith is used to get the Element object of the annotation ZiBindView
        Set<? extends Element> elements =roundEnvironment.getElementsAnnotatedWith(ZiBindView.class);

        ZiBindView = ZiBindView = ZiBindView
        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);
            }
            ZiBindView bindAnnotation = variableElement.getAnnotation(ZiBindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id,variableElement);

        }

        for(String key:mProxyMap.keySet()){
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE,"process finish ...");
        return true; }}Copy the code
        for(String key:mProxyMap.keySet()){
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            }catch(IOException e){ e.printStackTrace(); }}Copy the code
  • Returns the filer used to create the new class

ClassCreatorProxy complete code

public class ClassCreatorProxy {
    // The name of the bound class
    private String mBindingClassName;
    // The name of the package to bind
    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);
    }

    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();
    }


    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) {
        return mTypeElement;
    }
//JavaPoet is a build utility class framework for automatically generating Java files that makes it easy to build code at compile time based on what we annotate.
    public TypeSpec generateJavaCode2(a) {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods2())
                .build();
        return bindingClass;

    }

    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

Zi – API module

public class ZiBindViewTools {

    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
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
Copy the code

We’re through

We have finished the APT part of the code, so how to use we create ZiBindView? Use the butterKnife in the same way. ```java public class MainActivity extends AppCompatActivity { @ZiBindView(value = R.id.main_activity_textView) TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ZiBindViewTools.bind(this); textView.setText("Zidea"); }}Copy the code
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
Copy the code

However, when we run it, we may not be able to live the code because the gradle version is too high and we need to create it manually.

  • Start by switching the directory from Android to the Project view
  • inzi-compiler Create the following structure under the directoryresources/META-INF/servicesFile directory
  • Then add a question under the directoryfileAdd the following content to file

So we just created ZiBindViewProcessor package name plus file name.