This is the fifth day of my participation in the November Gwen Challenge. Check out the details: The last Gwen Challenge 2021

preface

In the last article, we started practicing Gradle in action. In this article, we will implement the BindView in ButterKnife to explain Gradle automatic code handling in practice.

1. The APT is introduced

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 a. Java file as output.

This is simply a.java file generated by annotations at compile time.

So how does the annotation handler work?

1.1 How do I use the annotation Processor

  • The annotationProcessor is as important a setting as the general implementation in dependency management
  • The annotation processor is used to scan and process specified annotation classes at compile time
  • In dependencies in {} in through annotationProcessor add library, is not packed into the file, instead of at compile time, run the specified Processor class, these are inherited AbstractProcessor Processor, And implements the process() method to handle annotations

2. The APT configuration

The library added by the annotationProcessor is described here, so create the corresponding library, and import it with the annotationProcessor, because it won’t be packaged into a file, so create java-library.

As is shown in

Create java-library project in the project project, enter the bulid.gradle project, and add the java-library project to the annotationProcessor

Project bulid. Gradle

android{ ... Slightly dependencies {... Slightly annotationProcessor project (':compiler')... Slightly}... Slightly}Copy the code

As mentioned earlier, the annotation handler is only used to process annotations, and the handler is not packaged into APK, but we use corresponding annotations when we use APT to automate code processing. That is, the annotation to be used is definitely not in the annotation handler. So define a separate library for annotations here.

Corresponding BindView annotation

@Retention(RetentionPolicy.RUNTIME)/ / runtime
@Target({ElementType.FIELD})// Modify attributes
public @interface BindView {
    int value(a);

}

Copy the code

Build. gradle corresponds to the annotation handler

plugins {
    id 'java-library'
    id 'kotlin'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
	// Automatically generate code
    annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 rc4'
    compileOnly 'com. Google. Auto. Services: auto - service: 1.0 rc4'
    // Annotations are used here, so it depends on the corresponding library for annotations
    implementation project(':annotation2') 
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'com. Squareup: javapoet: 1.13.0'
}
Copy the code

Latest project build.gradle

android{ ... Slightly dependencies {... Slightly implementation project (':annotation2')
	    annotationProcessor project(':compiler')... Slightly}... Slightly}Copy the code

Compiler code does not package into APK, while annotation2 annotations package into APK.

The next step is to implement APT.

3. The APT

These processors all inherit from AbstractProcessor. So let’s go with the above.

ButterKnifeProcessor.java

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

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

    // Do the initialization manipulation
    // Get some instances of the utility classes
    @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 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();
            mMessager.printMessage(Diagnostic.Kind.NOTE, "fullClassName " + fullClassName);

            // Information about elements is saved to mProxyMap
            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                // Avoid duplicate creation
                proxy = new ClassCreatorProxy(mElementUtils, classElement);
                //fullClassName indicates the corresponding class name
                mProxyMap.put(fullClassName, proxy);
            }

            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            // Get the annotation to identify the control ID and store the corresponding control ID and Element in the Map as key-value pairs
            proxy.putElement(id, variableElement);
        }

        // Generated by Javapoet
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "getPackageName " + proxyInfo.getPackageName().toLowerCase());
            mMessager.printMessage(Diagnostic.Kind.NOTE, "generateJavaCode " + proxyInfo.generateJavaCode());

            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode()).build();
            try {
                // Generate the file
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        returnSourceVersion.RELEASE_7; }}Copy the code

The annotation handler will automatically run the process() method in the specified Processor class.

This method is to obtain the corresponding annotations, get the corresponding annotations in the package name and the name of the class, and then through proxyInfo. GenerateJavaCode () to create the corresponding logic code, Finally, use javafile. writeTo to generate the corresponding MainActivity_ViewBinding file.

Note: This class must be identified with the @AutoService(Processor.class) annotation

Helper class: classCreatorProxy.java


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 * javapoet **@return* /
    public TypeSpec generateJavaCode(a) {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods2())
                .build();
        return bindingClass;

    }
    /** * add Method * javapoet */
    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

Now that the annotation handler has been written, let’s try using it in the corresponding Activity to see what happens:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_content)
    TextView textContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        // testutil.hello () this is the method inside aartextContent.setText(TestUtil.hello()); }}Copy the code

Run to see the effect:

As is shown in

APT has successfully generated the corresponding code for us, but in setText, there is a null pointer exception.

Come to think of it, APT generated the code for us, but it doesn’t seem to call the corresponding bind method.

How about manually calling bind via MainActivity_ViewBinding in the logic code?

MainActivity_ViewBinding (APT) does not require MainActivity_ViewBinding to be used in APT. How do I get the bind method in MainActivity_ViewBinding?

Since direct reminders don’t exist, try dynamic proxies!

Go to the library where the AAR was generated yesterday and create 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

Call the bind method by using method.invoke to call the corresponding method.

For the knowledge of dynamic proxy, you can read this article. I have explained the corresponding knowledge in detail here.

The aar library code has been modified so that yesterday’s references are no longer available, and the AAR will be repackaged and published in Maven in the same way as in the previous article.

The final activity code:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_content)
    TextView textContent;

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        BindViewTools.bind(this); textContent.setText( TestUtil.hello()); }}Copy the code

Here we call our dynamic proxy method, now run to see the effect:

Perfect run!

conclusion

I believe you have a deeper understanding of APT and Gradle. In the next installment, we will start working on Gradle componentization.