How to become a T – type talent, vertical in an industry, must have a setThe knowledge systemHere, just one word insist ~

preface

During the development process, data binding is often required. Since we write the code repeatedly every time, sometimes we wonder if we can compile boilerplate code using Annotation technology to improve the development efficiency. After all, programmers like to be lazy. This framework was developed a long time ago (ButterKnife). The internal source code is not complicated, but it is solved by compile-time annotations, APT technology. So how do you handwrite the framework (ButterKnife) for data binding? Next, direct coding, annotating theoretical knowledge, please move on to the details of annotating knowledge. Thank you ~

Contents

Define data binding annotations

  • BindView: This annotation addresses the findViewById control binding and automatically converts types. I won’t go into the details of how to create annotations here, see my other article on annotations in detail. @Retention(retentionPolicy. CLASS) : indicates compile-time annotations. @target (elementtype. FIELD) : indicates modification fields. Annotation parameters are defined as Int values, indicating control ID values.
/** * @Author: WeiShuai * @Time: 2020/5/13 * @Description: */ @retention (retentionPolicy.class) @target (elementtype.field) public @interface BindView {int value(); }Copy the code
  • OnClick: This annotation addresses the OnClickListener event binding. @Retention(retentionPolicy.class) represents compile-time annotations, @target (elementType.method) represents modification methods, and annotation parameters are defined as values of type Int[]. Internal very much according to Int[] type Id value, bind different events, handle different event logic.
/** * @Author: WeiShuai * @Time: 2020/5/13 * @Description: @retention (retentionPolicy.class) @target (elementtype.method) public @interface OnClick {int[] value(); }Copy the code

Custom annotation handlers

Different annotation types correspond to different annotation processors, which are mainly used for annotation parsing. To define compile-time annotations, we need to inherit AbstractProcessor to create an annotation processor and rewrite the parent class methods. In general, we just need to rewrite several important methods respectively:

Synchronized void init(ProcessingEnvironment) public Boolean process(Set<? extends TypeElement> set, RoundEnvironment RoundEnvironment) {3, annotated version public SourceVersion getSupportedSourceVersion (4), analytic type public Set < String > getSupportedAnnotationTypes()Copy the code

2.1. Annotation processor initialization

The AbstractProcessor annotation processor only calls the init(ProcessingEnvironment ProcessingEnvironment) method once per initialization, so it can be used to initialize utility classes

@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); / / annotation processor tool mProcessorUtils = ProcessorUtils getInstance (processingEnvironment); / / analytical annotation information mViewBindingGroupedClasses = new ViewBindingGroupedClasses (); MViewBindingGenerator = new ViewBindingGenerator(); }Copy the code

2.2. Bind annotations to annotation handlers

GetSupportedAnnotationTypes () method is the annotation processor can recognize annotation types. The annotations @bindView and @onClick are stored in the LinkedHashSet data structure, and the collection is returned to indicate that the annotation processor recognizes the target annotation object.

@Override
public Set<String> getSupportedAnnotationTypes() {
    LinkedHashSet<String> type = new LinkedHashSet<>(2);
    type.add(BindView.class.getCanonicalName());
    type.add(OnClick.class.getCanonicalName());
    return type;
}
Copy the code

2.3 annotation parsing processing

The RoundEnvironment class provides a #getElementsAnnotatedWith() method that returns all information about the annotation type based on that type. Element provides information about the annotation value. CheckValidFiled (fieldElement) is the verification of the @BindView modification variables, and checkMethodElement(executableElement) is the verification of the @onclick modification method. After the verification is successful, annotation information can be obtained. And use APT technology package into Codeblock. Builder. The ViewBindingGenerator class generates boilerplate code based on the codebLock. Builder configuration information.

@Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {extends TypeElement> set, RoundEnvironment RoundEnvironment) { extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class); // get @onclick information Set<? extends Element> onClickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class); Public View v for (Element fieldElement: bindViewElements) { if(checkValidFiled(fieldElement)) mViewBindingGroupedClasses.parseBindView(fieldElement); ExecutableElement public void OnClick (View View) {} for (Element executableElement: onClickElements) { if (checkMethodElement(executableElement)) mViewBindingGroupedClasses.parseListenerView(executableElement); } try {// Parse the annotation configuration information, And generate a written to the file HashMap < TypeElement, List < CodeBlock. Builder > > bindMaps = mViewBindingGroupedClasses. GetBindMaps (); if(bindMaps.size()>0) { for (Map.Entry<TypeElement, List<CodeBlock.Builder>> entry : bindMaps.entrySet()) { mViewBindingGenerator.generator(entry.getKey(), entry.getValue(), mProcessorUtils, processingEnv); } mViewBindingGroupedClasses.deleteAll(); } } catch (Exception e) { e.printStackTrace(); mProcessorUtils.eLog(e.getMessage()); } return true; }Copy the code

2.4. Annotation information verification

  1. Validates the variable access modifier, returning true if the variable access modifier is public, or false anyway.
/** * public View v; * @param element * @return */ private boolean checkValidFiled(Element element){ if(element==null) return false; // Determine the variable access modifier if(! element.getModifiers().contains(Modifier.PUBLIC)){ mProcessorUtils.eLog("The field $s not public", element.getSimpleName()); return false; } return true; }Copy the code
  1. Validates variable access modifiers, if method modifiers, method return type, number of method parameters.
/** * check method, format: public void onClick(View v){} * @param element * @return */ private boolean checkMethodElement(Element element){ if(element==null) return false; ExecutableElement executableElement=(ExecutableElement)element; // Access the modifier if(! executableElement.getModifiers().contains(Modifier.PUBLIC)){ mProcessorUtils.eLog("The method $s not public", element.getSimpleName()); return false; } / / judgment method return type TypeMirror returnType. = executableElement getReturnType (); if(returnType.getKind()! = TypeKind.VOID){ mProcessorUtils.eLog("The method $s return type not void", element.getSimpleName()); return false; } // The method returns the argument List<? extends VariableElement> parameters = executableElement.getParameters(); if(parameters.size()! =1){ mProcessorUtils.eLog("The method $s parameter size entry", element.getSimpleName()); return false; } return true; }Copy the code

Parsing annotation information to generate boilerplate code

3.1. Parse annotation information

  1. @viewBind annotation information parsing, get variable type, variable name, annotation parameter information, get data and encapsulate it in CodebLock. Builder, and successfully return codebLock. Builder object.
/** * parse the ViewBind annotation information, Build parseBindView(Element Element){// Read the variable type String type = element.asType().toString(); String name = element.getSimplename ().toString(); Int annotationValue = element.getannotation (bindview.class).value(); CodeBlock.Builder builder = CodeBlock.builder() .add("target.$L=",name) .add("($L)source.findViewById($L)",type,annotationValue); return builder; }Copy the code
  1. ** @onclick ** Annotation information that stores an array of event control ids, iterates over a number of sets of data and encapsulates them in codebLock. Builder, and returns the CodebLock. Builder object successfully.
/** * Parse OnClick annotation information and convert it to codeblock. builder data * @param Element * @return */ ArrayList< codeblock. builder > Int [] annotationValue = element.getannotation (onclick.class).value(); String name = element.getSimplename ().toString(); ArrayList<CodeBlock.Builder> builders = new ArrayList<>(); for(int value:annotationValue){ CodeBlock.Builder builder = CodeBlock.builder() .add("source.findViewById($L).setOnClickListener(new android.view.View.OnClickListener() " + "{ public void onClick(View  v) { target.$L(v); }})", value, name); builders.add(builder); } return builders; }Copy the code

3.2 encapsulate annotation information

Codeblock. Builder needs to differentiate between different types of TypeElement because TypeElement is required to fetch class information and generate boilerplate code from that class information.

public class ViewBindingGroupedClasses { private HashMap<TypeElement, List<CodeBlock.Builder>> bindMaps = new HashMap<>(); private ViewBindingClasses mViewBindingClasses; public ViewBindingGroupedClasses(){ mViewBindingClasses = new ViewBindingClasses(); } /** * ViewBind annotation information, * * @param Element */ public void parseBindView(Element element) {ElementKind kind = element.getKind(); if (kind == ElementKind.FIELD) { CodeBlock.Builder builder = mViewBindingClasses.parseBindView(element); if(builder! =null) { saveCodeBlockData(element, builder); }}} /** * onClick annotation information, * * @param Element */ public void parseListenerView(Element element) {ElementKind kind = element.getKind(); if (kind == ElementKind.METHOD) { ArrayList<CodeBlock.Builder> builder = mViewBindingClasses.parseListenerView(element);  if(builder! =null) { saveCodeBlockData(element, builder); }}} /** * * * @param Element */ private void saveCodeBlockData(Element element, ArrayList<CodeBlock.Builder> codeBlockList) { TypeElement typeElement = (TypeElement) element.getEnclosingElement(); List< codeblock. Builder> codeBlockLists = bindmaps. get(TypeElement); if (codeBlockLists == null) { codeBlockLists = new ArrayList<>(); } // save CodeBlock data if (codeBlockList! = null) { codeBlockLists.addAll(codeBlockList); } bindMaps.put(typeElement, codeBlockLists); } /** * * * @param Element */ private void saveCodeBlockData(Element element, CodeBlock.Builder codeBlock) { TypeElement typeElement = (TypeElement) element.getEnclosingElement(); List< codeblock. Builder> codeBlockLists = bindmaps. get(TypeElement); if (codeBlockLists == null) { codeBlockLists = new ArrayList<>(); } // save CodeBlock. = null) { codeBlockLists.add(codeBlock); } bindMaps.put(typeElement, codeBlockLists); Public void deleteCodeBlockData(element element) {TypeElement typeElement = (TypeElement) element.getEnclosingElement(); List< codeblock. Builder> codeBlockLists = bindmaps. get(TypeElement); if (codeBlockLists ! = null) { bindMaps.remove(typeElement); }} /** * clear all data */ public void deleteAll(){bindmaps.clear (); Public HashMap<TypeElement, List< codeblock. Builder>> getBindMaps() {return bindMaps; }}Copy the code

3.3. Generate boilerplate code

Public class ViewBindingGenerator {/** ** @param typeElement * @param codeBlocks configuration information * @param processorUtils * @param  processingEnv */ public void generator(TypeElement typeElement, List<CodeBlock.Builder> codeBlocks, ProcessorUtils processorUtils, ProcessingEnvironment processingEnv) {/ / get the name of the class String className = typeElement. GetSimpleName (), toString (); TypeName TypeName = TypeName. Get (typeElement.astype ()); if(typeName instanceof ParameterizedTypeName){ typeName=((ParameterizedTypeName)typeName).rawType; } // Configure the constructor, And set the parameter MethodSpec. Builder methodBuilder = MethodSpec. ConstructorBuilder () addModifiers (Modifier. The PUBLIC) .addParameter(typeName,"target",Modifier.FINAL) .addParameter(ClassName.get("android.view", "View"), "source",Modifier.FINAL); / / configuration constructor content information for (CodeBlock. Builder Builder: codeBlocks) {methodBuilder. AddStatement (Builder. The build ()); } processorUtils.writeToFile( className+ Constant.VIEW_BINDING_SUFFIX, processorUtils.getPackageName(typeElement), methodBuilder.build(), processingEnv, null); }}Copy the code

4. Function point test

4.1. Test the code

public class ThreeActivity extends AppCompatActivity { @BindView(R.id.mTvTitle) public TextView mTvTitle; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_three); AnnotationUtils.bind(this); } @OnClick({R.id.mBtSubmit}) public void onClick(View view){ if(view.getId()==R.id.mBtSubmit){ Toast.maketext (this," click ", toast.length_short).show(); }}}Copy the code

4.2 boilerplate code

public final class ThreeActivity$ViewBinding {
  public ThreeActivity$ViewBinding(final ThreeActivity target, final View source) {
    target.mTvTitle=(android.widget.TextView)source.findViewById(2131165293);
    source.findViewById(2131165291).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onClick(v); }});
  }
}
Copy the code

About me