Android advanced light learning record -- an attempt of ButterKnife, an annotation and dependency injection framework

Posted by Debbie-Leigh on Mon, 27 Dec 2021 21:11:20 +0100

⚠️ The module created is a java module rather than an Android Library. If the latter is created, AbstractProcessor cannot be used

 

According to the book, I want to imitate butterknife myself

The final project structure is shown in the figure above. Among them, annotations is a Java library with different annotations, process is an annotation processor and a Java library, and butterKnife is an Android library to implement the final onbind() method

 

The final generated file is in this location (I found it wrong at the beginning. I thought it was checked many times under resource, but I didn't find it. Finally, I searched globally and found that the final generated file may be in this path because of the problems of as and gradle versions)

The essence of ButterKinfe is to write findViewById and OnClick instead of ourselves, which liberates our hands and uses BindView and other annotations to implement the corresponding methods. However, in essence, it still needs to find the resource id through findViewById() and bind it with the control, as shown in the following figure

Therefore, the essence of imitating ButterKnife is to define similar annotations and generate corresponding files and functions.

1. Create a new Java Library in the project to store annotations. The Library is called annotations, similar to the following figure

The Java library in the build.exe Gradle is configured as follows

plugins {
    id 'java-library'
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

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

 2. In the project, create a new Java Library to store the annotation Processor. This Library is called process (pay attention to the path. I accidentally lost some paths at the beginning of writing, resulting in failure to generate correct results). Since process is an annotation Processor, it is necessary to manually or automatically generate the Processor file, as shown in the following figure

I use Google's open source autoservice, build Gradle is configured as follows

plugins {
    id 'java-library'
}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
    implementation project(path: ':annotations')
}

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

And add @ AutoService (Processor.class) to the processor

 

Take a simple example, BindString

Before ButterKnife, use string, similar to

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text_view)
    TextView textView;
    //@BindString(R.string.app_name)
    String meg = null;

    @OnClick(R.id.text_view)
    public void onClick(View view){
        switch (view.getId()){
            case R.id.text_view:
                meg = getResources().getString(R.string.app_name);
                Toast.makeText(MainActivity.this,meg,Toast.LENGTH_LONG).show();
                break;
        }
    }

After use

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text_view)
    TextView textView;
    @BindString(R.string.app_name)
    String meg;

    @OnClick(R.id.text_view)
    public void onClick(View view){
        switch (view.getId()){
            case R.id.text_view:
                Toast.makeText(MainActivity.this,meg,Toast.LENGTH_LONG).show();
                break;
        }
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        TextView textView = findViewById(R.id.text_view);
    }
}
Generated MainActivity$ViewBinder file
package com.example.farmedemo;
import android.view.View;
public class MainActivity$ViewBinder{
public MainActivity$ViewBinder(final com.example.farmedemo.MainActivity target){
target.textView =(android.widget.TextView)target.findViewById(2131230975);
(target.findViewById(2131230975)).setOnClickListener(new View.OnClickListener() {
public void onClick(View p0) {
target.onClick(p0);
}
});
target.meg =(java.lang.String)target.getBaseContext().getResources().getString(2131623963);
}
}

AnnotationCompiler
package com.example.process;

import com.example.annotations.BindString;
import com.example.annotations.BindView;
import com.example.annotations.OnClick;
import com.google.auto.service.AutoService;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
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.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

@AutoService(Processor.class)
public class AnnotationCompiler extends AbstractProcessor {
  //The object that generated the file
  Filer filer;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //Initializes the object that generated the file
    filer = processingEnvironment.getFiler();
  }

  /**
   * Declare the annotation to be processed by the annotation processor
   * @return
   */
  @Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new HashSet<>();
    types.add(OnClick.class.getCanonicalName());
    types.add(BindView.class.getCanonicalName());
    types.add(BindString.class.getCanonicalName());
    return types;
  }

  /**
   * Declare the java source version supported by the annotation processor
   * @return
   */
  @Override
  public SourceVersion getSupportedSourceVersion() {
    return processingEnv.getSourceVersion();
  }

  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //Get the annotation in the program and correspond to the class object it belongs to one by one
    Map<TypeElement, ElementForType> andParseTargets = findAndParseTargets(roundEnvironment);
    //Write file
    if(andParseTargets.size()>0){
      Iterator<TypeElement> iterator = andParseTargets.keySet().iterator();
      Writer writer = null;
      while (iterator.hasNext()){
        TypeElement typeElement = iterator.next();
        ElementForType elementForType = andParseTargets.get(typeElement);
        String activityName = typeElement.getSimpleName().toString();
        //Get new class name
        String newClazzName = activityName+"$ViewBinder";
        //Get package name
        String packageName = getPackageName(typeElement);
        //Create a java file
        try {
          JavaFileObject sourceFile = filer.createSourceFile(
          packageName+ "." + newClazzName);
          writer = sourceFile.openWriter();
          StringBuffer stringBuffer = getStringBuffer(packageName, newClazzName, typeElement, elementForType);
          writer.write(stringBuffer.toString());
        } catch (IOException e) {
          e.printStackTrace();
        }finally {
          if(writer != null){
            try {
              writer.close();
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      }
    }
    return false;
  }

  /**
   * Get all the annotations and map the annotations to the Activity one by one
   * @param roundEnvironment
   */
  private Map<TypeElement, ElementForType> findAndParseTargets(RoundEnvironment roundEnvironment) {
    Map<TypeElement, ElementForType> map = new HashMap<>();

    //Get all nodes in the module that use BindView
    Set<? extends Element> viewElelments = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    //Get all nodes in the module that use OnClick
    Set<? extends Element> methodElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
    //Get all nodes in the module that use BindString
    Set<?extends Element> stringElements = roundEnvironment.getElementsAnnotatedWith(BindString.class);
    //Traverse all member variable nodes and encapsulate them into ElementForType object one by one
    for (Element viewElelment : viewElelments) {
      //transformation
      VariableElement variableElement = (VariableElement) viewElelment;
      //Get its previous node. The previous node of the member variable node is the class node
      TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
      //Get the value corresponding to the class node in the map
      ElementForType elementForType = map.get(typeElement);
      List<VariableElement> viewElements;
      //If this class node already exists in the map
      if(elementForType !=null){
        //Gets the collection of control nodes in the value corresponding to the class node
        viewElements = elementForType.getViewElements();
        //If the collection is empty, a new collection is created and placed in the value corresponding to the class node
        if(viewElements == null){
          viewElements = new ArrayList<>();
          elementForType.setViewElements(viewElements);
        }
      }else{
        //If elementForType is empty, a new one is created
        elementForType = new ElementForType();
        //Also create a new collection of control nodes
        viewElements = new ArrayList<>();
        elementForType.setViewElements(viewElements);
        if(!map.containsKey(typeElement)){
          map.put(typeElement,elementForType);
        }
      }
      //Finally, the object of the node traversed by the control is placed in the node encapsulation class of the control
      viewElements.add(variableElement);
    }
    //Traverse the nodes of all click event methods and encapsulate them in objects
    for (Element methodElement : methodElements) {
      ExecutableElement executableElement = (ExecutableElement) methodElement;
      TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
      ElementForType elementForType = map.get(typeElement);
      List<ExecutableElement> executableElements;
      logUtil(elementForType+"");
      if(elementForType !=null){
        executableElements = elementForType.getMethodElements();
        if(executableElements == null){
          executableElements = new ArrayList<>();
          elementForType.setMethodElements(executableElements);
        }
      }else{
        elementForType = new ElementForType();
        executableElements = new ArrayList<>();
        elementForType.setMethodElements(executableElements);
        if(!map.containsKey(typeElement)){
          map.put(typeElement,elementForType);
        }
      }
      executableElements.add(executableElement);
    }
    //Traverse member variable nodes and encapsulate
    for (Element viewElelment : stringElements) {
      //transformation
      VariableElement variableElement = (VariableElement) viewElelment;
      //Get its previous node. The previous node of the member variable node is the class node
      TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
      //Get the value corresponding to the class node in the map
      ElementForType elementForType = map.get(typeElement);
      List<VariableElement> viewElements;
      //If this class node already exists in the map
      if(elementForType !=null){
        //Gets the collection of control nodes in the value corresponding to the class node
        viewElements = elementForType.getStringElements();
        //If the collection is empty, a new collection is created and placed in the value corresponding to the class node
        if(viewElements == null){
          viewElements = new ArrayList<>();
          elementForType.setStringElements(viewElements);
        }
      }else{
        //If elementForType is empty, a new one is created
        elementForType = new ElementForType();
        //Also create a new collection of control nodes
        viewElements = new ArrayList<>();
        elementForType.setStringElements(viewElements);
        if(!map.containsKey(typeElement)){
          map.put(typeElement,elementForType);
        }
      }
      //Finally, the object of the node traversed by the control is placed in the node encapsulation class of the control
      viewElements.add(variableElement);
    }

    return map;
  }

  /**
   * Method to get package name
   * @param typeElement
   */
  public String getPackageName(Element typeElement){
    //Get package name
    PackageElement packageOf = processingEnv.getElementUtils().getPackageOf(typeElement);
    Name qualifiedName = packageOf.getQualifiedName();
    return qualifiedName.toString();
  }

  public void logUtil(String message){
    Messager messager = processingEnv.getMessager();
    messager.printMessage(Diagnostic.Kind.NOTE,message);
  }

  /**
   * Gets the method of the assembly statement of the class
   * @param packageName
   * @param newClazzName
   * @param typeElement
   * @param elementForType
   * @return
   */
  public StringBuffer getStringBuffer(String packageName,String newClazzName,
                                      TypeElement typeElement,ElementForType elementForType ) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("package " + packageName + ";\n");
    stringBuffer.append("import android.view.View;\n");
    stringBuffer.append("public class " + newClazzName + "{\n");
    stringBuffer.append("public " + newClazzName + "(final " + typeElement.getQualifiedName() + " target){\n");
    if (elementForType != null && elementForType.getViewElements() != null && elementForType.getViewElements().size() > 0) {
      List<VariableElement> viewElements = elementForType.getViewElements();
      for (VariableElement viewElement : viewElements) {
        //Get type
        TypeMirror typeMirror = viewElement.asType();
        //Gets the name of the control
        Name simpleName = viewElement.getSimpleName();
        //Get resource ID
        int resId = viewElement.getAnnotation(BindView.class).value();
        stringBuffer.append("target." + simpleName + " =(" + typeMirror + ")target.findViewById(" + resId + ");\n");
      }
    }


    if (elementForType != null && elementForType.getMethodElements() != null && elementForType.getMethodElements().size() > 0) {
      List<ExecutableElement> methodElements = elementForType.getMethodElements();
      for (ExecutableElement methodElement : methodElements) {
        int[] resIds = methodElement.getAnnotation(OnClick.class).value();
        String methodName = methodElement.getSimpleName().toString();
        for (int resId : resIds) {
          stringBuffer.append("(target.findViewById(" + resId + ")).setOnClickListener(new View.OnClickListener() {\n");
          stringBuffer.append("public void onClick(View p0) {\n");
          stringBuffer.append("target." + methodName + "(p0);\n");
          stringBuffer.append("}\n});\n");
        }
      }
    }

    if (elementForType != null && elementForType.getStringElements() != null && elementForType.getStringElements().size() > 0) {
      List<VariableElement> variableElements = elementForType.getStringElements();
      for (VariableElement variableElement : variableElements) {
        //Get type
        TypeMirror typeMirror = variableElement.asType();
        //Gets the name of the control
        Name simpleName = variableElement.getSimpleName();
        //Get resource ID

        int resId = variableElement.getAnnotation(BindString.class).value();
        stringBuffer.append("target." + simpleName + " =(" + typeMirror + ")target.getBaseContext().getResources().getString(" + resId + ");\n");
        stringBuffer.append("}\n}\n");

      }
//    }

      return stringBuffer;
    }
    return stringBuffer;
  }
}
ElementForType
package com.example.process;

import java.util.List;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;

public class ElementForType {
  //A collection of all nodes bound to the View's member variables
  List<VariableElement> viewElements;
  //A collection of nodes for all click time methods
  List<ExecutableElement> methodElements;
  List<VariableElement> stringElements;

  public List<VariableElement> getStringElements() {
    return stringElements;
  }

  public void setStringElements(List<VariableElement> stringElements) {
    this.stringElements = stringElements;
  }

  public List<VariableElement> getViewElements() {
    return viewElements;
  }

  public void setViewElements(List<VariableElement> viewElements) {
    this.viewElements = viewElements;
  }

  public List<ExecutableElement> getMethodElements() {
    return methodElements;
  }

  public void setMethodElements(List<ExecutableElement> methodElements) {
    this.methodElements = methodElements;
  }
}

It can be seen that butterknife has done a lot for us. The above code is very different from the real butterknife. Butterknife is generally not used for component development because it will lead to the repetition of control Id.

Topics: Java Android Apache