This post was published at mrrobot97.me, a personal blog I’ve been shelving for a year
Most Android developers have already used ButterKnife, which allows you to quickly bind entity views to XML, as well as resources, animations, strings, and even click events. The idea behind ButterKnife is to dynamically generate code and bind ids to our view using custom annotations + custom annotations parser. This article shows how to customize annotations + annotation parsers by implementing a demo ButterKnife project.
There is not much introduction to annotations in this paper. Here is an article on annotations. It takes one hour to understand custom annotations, and readers who are still unfamiliar with annotations can take a look at the knowledge of annotations.
Create a new Android Studio Project and call it MyButterKnife. MainActivity and layout are automatically generated. Add an ID to the TextView in activity_main.xml.
Next, create a new module that implements our custom annotations and our custom annotation parser. Note that this module must be the Java Library, where we can inherit the parser AbstractProcessor. The Android Library is not accessible.
Create a new Java Library and name it Processor.
Then customize annotations. We just do a demo experiment, so we only implement the binding function of View and ID. Here I define two annotations NeedBind and BindView:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface NeedBind {
}
Copy the code
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value(a) default- 1;
}
Copy the code
NeedBind’s Target TYPE indicates that this is an annotation that is used to modify classes and interfaces. NeedBind’s purpose here is to help us quickly filter out classes that need to handle custom annotations. The Target of a BindView is a FIELD, or member variable, which is the view member to which the resource ID is bound.
Both annotations have CLASS Retention, meaning that annotations are compiled and retained in.class files but not at RUNTIME and therefore do not affect the performance of the code at RUNTIME. A trick is to name the annotation variable value(if there is only one variable). You can omit the name of the annotation variable when declaring it.
@BindView(R.id.my_tv)
TextView mTV;
Copy the code
If we call it something else like id, then the annotation must be used like this:
@BindView(id = R.id.my_tv)
TextView mTv;
Copy the code
Once annotations are defined, they can be used in projects:
@NeedBind
public class MainActivity extends AppCompatActivity {
@BindView(R.id.my_tv)
TextView mTv; // Cannot be private
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}Copy the code
Notice that I added two annotations: NeedBind for MainActivity and BindView for mTv. Another important note is mTv variables can’t use private decorate, because we are through the generated proxy class invokes the MainActivity. View = (view) MainActivity. The findViewById () to implement for the bundle id of the view, So mTv needs to be at least package visible. We haven’t parsed our custom annotations yet, so the annotations we add are useless, so let’s start implementing our annotation parser.
Again, in the Processor Module, create a new class MyButterKnifeProcessor that inherits from AbstractProcessor. This is the parser used to parse custom annotations. For this to work, however, you must create the following directory structure under the processor:
And called new javax.mail. The annotation. Processing. The Processor of text files, content is a line:
me.mrrobot97.lib.MyButterKnifeProcessor
Copy the code
You also need to modify the build.gradle file in your App Module to add:
compile project(path: ':processor')
annotationProcessor project(path: ':processor')
Copy the code
This is done so that the compiler can use our parser to parse annotations.
The rest is done in the MyButterKnifeProcessor class. Our goal is to generate the code for the binding view by reading the custom annotations in the class. This requires a library that generates Java code, javapoet, produced by SQure, which is absolutely superb. Add the following line to the processor’s build.gradle:
compile 'com. Squareup: javapoet: 1.9.0'
Copy the code
Ps: Such a practical open source project is only 4500start on Github, which is not as popular as the star of the recently popular wechat jump jump game auxiliary script. I am also drunk. Github’s star is still very low, just take a look at it, don’t judge a project by the number of stars…
MyButterKnifeProcessor method needs to be rewritten in the process () method and getSupportedAnnotationTypes () :
public class MyButterKnifeProcessor extends AbstractProcessor{
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// Generate proxy classes for all classes annotated with NeedBind
for(Element element:roundEnvironment.getElementsAnnotatedWith(NeedBind.class)){
generateBinderClass((TypeElement) element); // implement later
}
//return true indicates whether the annotations processed by this processor are processed only by this processor
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes(a) {
// indicates that the annotation handler needs to handle classes and interfaces with these annotations
returnCollections.singleton(NeedBind.class.getCanonicalName()); }}Copy the code
Then comes the crux of this article: handling annotations and generating helper classes. It is highly recommended that you read ** the simple use of Javapoet ** first, otherwise it may be difficult to read the code that follows.
To show you what the resulting code looks like, here’s a demo that I practiced while preparing this article:
// This file is generated by Binder, do not edit!
package guru.mrrobot97.customannotationprocessor;
import android.view.View;
import android.widget.TextView;
public class MainActivityDeleagteBinder {
public MainActivityDeleagteBinder(final MainActivity activity) {
bindView(activity);
bindClick(activity);
}
private void bindView(final MainActivity activity) {
activity.mTv=(TextView)activity.findViewById(2131165301);
activity.mTv2=(TextView)activity.findViewById(2131165302);
}
private void bindClick(final MainActivity activity) {
activity.findViewById(2131165301).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) { activity.sayHello(); }}); }}Copy the code
All of the above content was generated by Javapoet, so let’s take a step by step analysis of how to generate our proxy class with this final result. For simplicity’s sake, we won’t generate bindClick code because we haven’t defined annotations.
We will generate a class named *DeleagteBinder for all classes annotated with NeedBind. Again, for simplicity we will only bind the View in the Activity. The DeleagteBinder class contains a constructor, a bindView method, and a bindView binding ID for the Activity with the bindView annotation. In addition, both the constructor and bindView method have a <? Extends Activity> parameter.
Let’s build it one by one, starting with our <? Extends Activity> method parameters:
// Get the Activity class
ClassName activityClassName=ClassName.get(element);
// Construct an activity type parameter
ParameterSpec activityParam=ParameterSpec.builder(activityClassName,"activity")
.addModifiers(Modifier.FINAL)
.build();
Copy the code
Then add the following method to find all member variables in the class that have some kind of annotation (VariableElement):
/** * Returns all member variables annotated with clazz-type annotations *@param typeElement
* @param clazz
* @return* /
private List<VariableElement> getFieldElementsWithAnnotation(TypeElement typeElement,Class clazz){
List<VariableElement> elements=new ArrayList<>();
for(Element element:typeElement.getEnclosedElements()){
if(element.getAnnotation(clazz)! =null) {// There is no type or access check, as there must be in a real production environmentelements.add((VariableElement) element); }}return elements;
}
Copy the code
View =activity.findViewById (); view=activity.findViewById ();
List<VariableElement> bindViewFieldList=getFieldElementsWithAnnotation(element,BindView.class);
CodeBlock.Builder bindViewCodeBlockBuilder=CodeBlock.builder();
for(VariableElement variableElement:bindViewFieldList){
// Get the variable name
String variableName=variableElement.getSimpleName().toString();
// The type of the variable
TypeName viewType=ClassName.bestGuess(variableElement.asType().toString());
// The value of the annotation, which is the ID to which the view is bound
int viewId=variableElement.getAnnotation(BindView.class).value();
bindViewCodeBlockBuilder.addStatement("activity.$L=($T)activity.findViewById($L)",variableName,viewType,viewId);
}
Copy the code
With bindView()’s method body and arguments, we can construct bindView() :
// Generate the bindView() method
MethodSpec bindViewMethod=MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC)
.addParameter(activityParam)
.addCode(bindViewCodeBlockBuilder.build())
.returns(void.class)
.build();
Copy the code
Constructor:
// The constructor calls the bindView method internallyMethodSpec constructorMethod=MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(activityParam) .addStatement("$N($L)",bindViewMethod,activityParam.name)
.build();
Copy the code
Then generate the *DelegateBinder class file:
// Generate the BinderDelegate class
String binderClassName=element.getSimpleName().toString();
TypeSpec delegateType=TypeSpec.classBuilder(binderClassName+"DelegateBinder")
.addModifiers(Modifier.PUBLIC)
.addMethod(bindViewMethod)
.addMethod(constructorMethod)
.build();
JavaFile javaFile=JavaFile.builder(getPackage(element).getQualifiedName().toString(),delegateType)
.addFileComment("This file is generated by Binder, do not edit!")
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
Copy the code
Note that the package name of the generated class should be the same as the package name of the Activity to be bound to, so that the member variables modified by BindView need to be visible within the package, otherwise they must be public. Get the package name as follows:
/** * find package name *@param element
* @return* /
public static PackageElement getPackage(Element element) {
while(element.getKind() ! = PACKAGE) { element = element.getEnclosingElement(); }return (PackageElement) element;
}
Copy the code
Finish all these above, Make the Project, you will find the build/generated under the app/source/apt/debug directory or become MainActivityDelegateBinder class:
Here, have success is very close to the distance, we also need to do is in the MainActivity the setContentView () call, after new out our MainActivityDelegateBinder class, This completes the ID binding of the MainActivity member variable with the BindView annotation. For new a MainActivityDelegateBinder, we create a new helper classes in the app module MyButterKnife:
public class MyButterKnife {
public static final String ACTIVITY_DELEGATE_SUFFIX = "DelegateBinder";
public static void bind(Activity activity){
String activityName=activity.getClass().getName();
String delegateName=activityName+ ACTIVITY_DELEGATE_SUFFIX;
try {
Class delegateClass=activity.getClass().getClassLoader().loadClass(delegateName);
Constructor constructor=delegateClass.getConstructor(activity.getClass());
constructor.newInstance(activity);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch(ClassNotFoundException e) { e.printStackTrace(); }}}Copy the code
Only slightly used in MyButterKnife reflected the new MainActivityDelegateBinder entity, Then MainActivityDelegateBinder constructor calls the bindView () finally realize the view in the MainActivity binding.
Call myButterknife.bind (this) from MainActivity:
@NeedBind
public class MainActivity extends AppCompatActivity {
@BindView(R.id.my_tv)
TextView mTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
mTv.setText("This is not hello world"); }}Copy the code
Compile and run without NullPointerExceptions, and the content of mTv is also the content we set:
At this point, our goal of implementing the Demo version of ButterKnife is almost complete!
Ps: If you use the Modifier in your custom Processor, please ignore it. This is a bug in Android Studio and will not affect compilation.
Again, the purpose of this article is to give readers a starting point for AnnotationProcessor, and the resulting Demo is also a very poor version, which can only be said to run without any security checks for legitimacy, type matching, access rights, etc., which is completely unavailable in the production environment. The real ButterKnife does a lot of security checking on these possible exceptions.
Also attached demo source address
My level is general, the article will inevitably have mistakes, please include.
The above.