Implementing a simple ButterKnife with annotations
✍ write by NamCooper
I. Basic knowledge of annotations
brief introduction
In development, we often see @Override, @Deprecated, @SuppressWarnings, which are common annotations. These annotations are equivalent to tags, with which compilers, development tools, or other programs can operate accordingly.
The three common annotations mentioned above are marked in methods, while in Java such markup can be used in package, class, field, method, method parameters and local variables. It has a wide range of uses, and there are many places worth studying and thinking about. Of course, this article is only about the simple use of annotations, but it's just about making mistakes.
Annotation type and how to define an annotation
- Types of annotations
Annotations in java support eight basic data types, String, Class, enum, annotation, and array types of the above types. - Define an annotation class
The following code is the first step in customizing annotations, which is also necessary when using annotations in Android development.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();//When using annotations, you can omit "value=" if you only assign values to attributes called values.
String name() default "zhangsan";//Default values
}
The above code declares an annotation class, ViewInject, and then a custom annotation in the format @ViewInject() will be used where the annotation is used later. @ Target and @Retention are called meta-annotations, and meta-annotations are also referred to as @Documented and @Inherited, which are not covered in this article, and can be studied by yourself if you are interested.
|| 1. @Target: Declares the scope of the object modified by this annotation class. In the example above, @Target(ElementType.FIELD) means that this annotation can only be used to modify member variables, as follows
class Test{
@ViewInject(value = 1)
int a;
public void xxx(){
...
};
}
Annotations acting on member variables are declared and cannot be used elsewhere. The following is the scope enumeration:
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
private ElementType() {
}
Commonly used are TYPE (acting on class), FIELD (acting on member variables), METHOD (acting on method), CONSTRUCTOR (acting on construction method). In the later use will be involved.
|| 2, @Retention: The preservation strategy for annotations is also an enumeration value.
RetentionPolicy.SOURCE: Annotations are stored only in source code, that is. java files
RetentionPolicy.CLASS: Annotations are stored in bytecode, that is. class file
RetentionPolicy.RUNTIME: Annotations stored in memory bytecodes that can be used for reflection
|| 3. int value(): Define an attribute of type int. Here you can see the annotation class as a common bean class, and int value() as an int value, which will be explained later.
|| 4. String name() default "zhangsan": Ibid., default defines the default value of this property.
Basic Application of Annotations
1. Define an annotation class
package demo;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.lang.model.element.Element;
@Target(ElementType.METHOD)//Acting on @Retention(RetentionPolicy.RUNTIME)//Runtime Annotation
public @interface UseCase {
int id();
String description() default "no description";
}
2. Use custom annotations
package demo;
public class PasswordUtil {
@UseCase(id = 1,description = "hahaha")//Use custom annotations and give two attributes public void outPut(UseCase useCase){
System.out.println("Re-execution id = " + useCase.id() + ":description = "+ useCase.description());
}}
3. Analytical Annotations
package demo;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
Method m = null;
try {//A Method of Obtaining Annotations by Reflection
Class c = Class.forName("demo.PasswordUtil");
m = c.getDeclaredMethod("outPut", UseCase.class);
}catch (NoSuchMethodException | SecurityException e){
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (m != null) {//Get annotation class objects and print information
UseCase useCase = m.getAnnotation(UseCase.class);
PasswordUtil util = new PasswordUtil();util.outPut(useCase);}}}
The results are as follows:
id = 1:description = hahaha Execute id = 1:description = hahaha again
It is not difficult to find that the annotated Method method Method object is obtained by reflection and annotated class is specified by getAnnotation Method. The annotated class object can be obtained by calling the defined attribute id(), description() in the annotated class object to obtain the value entered in the annotation. The interaction between annotations and annotated methods can be easily achieved by an intermediate class Test.
The above is an introduction to the basic usage of annotations in java, understanding the basic usage, and the following is the key point - how to use annotation mechanism to implement ButterKnife-like functions in Android.
Second, customize ButterKnife (if you paste the following code, please delete Chinese comments in the code!)
Sketch:
Early ButterKnife used runtime annotations, that is, the @Retention Policy.RUNTIME used above. The operation of findViewById implemented by annotations is similar to the principle mentioned above. Both of them use reflection to get annotations and perform corresponding operations while the program is running. The drawbacks of this method are obvious.
In an Android program, even in an activity, the number of view s can be very large, and the number of findViewById is very frequent. Frequent reflection operations are bound to affect performance, so ButterKnife updates abandon this implementation and change to compile-time annotation @Retention (Retention Policy. CLASS). The biggest difference between the two is that the original need for reflection to do things, now at compile time has been basically completed and generated source code, when the user calls a simple call to the bind method, you can complete the findViewById operation by calling the generated source code.
Next, let's do it ourselves!
+ Custom Annotation Moudle
- Create a java library "annotation" and note that it must be a java library!
- Create an annotation class
package com.namcooper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindField {
int value();//All you need here is an int value, and you need to pass in the control id
}
+ Custom Processor Moudle
- Create a java library "compiler", pay attention to the java library!
- Adding dependencies
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
//This reference is optional for creating resources/META-INF/javax.annotation.processing.Processor in the main folder and registering our custom processor. Of course, you can also register by creating and writing it manually.
compile 'com.google.auto.service:auto-service:1.0-rc2'Processor
//This reference is also optional to help us generate the source code, so that the generated source code can be well formatted, of course, can also be written manually.
compile 'com.squareup:javapoet:1.7.0'
compile project(':annotation')//Here you rely on the annotation class java Moudle just defined
}
- Create a processor
package com.compiler;
import com.google.auto.service.AutoService;
import com.namcooper.BindField;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
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.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
@AutoService(Processor.class)//Register processors through annotations, not omitted
public class BindProcessor extends AbstractProcessor {
/**
* Each annotated processor class must have a parametric-free constructor.
* init The method is called by apt when Processor is created and initialization is performed. For example, processingEnv.getFiler() used later can be retrieved once in this method.
* @param processingEnv Provides a series of annotation processing tools.
**/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override//Returns the supported Annotation type, where a collection is returned, so multiple types can be returned
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindField.class.getCanonicalName());
}
@Override//This method will be called after the annotation is scanned
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
//Get the node annotated by BindField (add your own knowledge about the node), where you get the collection of element objects of annotated member variables
Set<? extends Element> elements = environment.getElementsAnnotatedWith(BindField.class);
//Api creation method calling javaopet
MethodSpec.Builder findViewBuilder = MethodSpec.methodBuilder("findView")
.addModifiers(Modifier.PUBLIC)//Add declarations
.returns(void.class)//Add return value type
.addParameter(Object.class, "activity");//Add method parameters, referencing type and formal parameter name, respectively
String packageName = "";
String className = "";
for (Element el : elements) {//Traversing the node set to get relevant information
PackageElement packageElement = getPackage(el);
packageName = packageElement.getQualifiedName().toString();//Gets the package name of the package in which the annotated element resides
TypeElement typeElement = getClass(el);
className = typeElement.getSimpleName().toString();//Gets the class name of the class in which the annotated element resides
TypeMirror mirror = el.asType();//Get the reference type of the annotated element, such as TextView
String parameterName = el.getSimpleName().toString();//Gets the variable name of the annotated element
BindField bindView = el.getAnnotation(BindField.class);//Getting Annotation Class Objects
//According to the information obtained above, stitching method body, the grammar rules here can be referred to [basic use of javaopet] (http://blog.csdn.net/crazy1235/article/details/51876192)
findViewBuilder.addStatement("(($N)activity).$N = ($N)((android.app.Activity)activity).findViewById($L)", className, parameterName, mirror.toString(), bindView.value());
}
//Creating methods
MethodSpec findView = findViewBuilder.build();
//To create a class, you should pay attention to the custom naming rules, and unify the "original class name _Binder" with the subsequent use.
TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
//addSuperinterface(ClassName.get("com.namcooper.apt.bind_api","Binder")// / Add the interface implemented by this class, you need to specify the package name and the interface name.
.addMethod(findView)//Write the method just generated
.build();
//Because the process method is executed many times to ensure that annotations in the newly generated code are scanned, it can be truncated if the package name class name is empty.
if (packageName.equals("") || className.equals(""))
return true;
JavaFile javaFile = JavaFile.builder(packageName, helloWorld)//Specify package name and class to generate javaFile
.build();
try {
javaFile.writeTo(processingEnv.getFiler());//Write JavaFile to the default path: under the project build-generated-source-apt-debug, you can also freely specify the write path
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* Get PackageElement
*
* @throws NullPointerException If element is null
*/
private static PackageElement getPackage(Element element) {
while (element.getKind() != ElementKind.PACKAGE) {
element = element.getEnclosingElement();
}
return (PackageElement) element;
}
/**
* Get TypeElement
*
* @throws NullPointerException If element is null
*/
private static TypeElement getClass(Element element) {
while (element.getKind() != ElementKind.CLASS) {
element = element.getEnclosingElement();
}
return (TypeElement) element;
}
}
+ Try to use
Adding dependencies to app
compile project(':annotation')
annotationProcessor project(':compiler')
Add two TextView s to the layout of MainActivity under app and use the annotations just defined in MainActivity.
TextView
android:id="@+id/xxx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
TextView
android:id="@+id/ddd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
package com.namcooper.apt.androidapt;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import com.namcooper.BindField;
public class MainActivity extends Activity {
@BindField(R.id.xxx)
TextView demoX;
@BindField(R.id.ddd)
TextView demoD;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Here's the moment to witness miracles: Rebuild Project
This location found a java file named after the naming rules we set. Open it and see!
package com.namcooper.apt.androidapt;
import java.lang.Object;
public final class MainActivity_Binder {
public void findView(Object activity) {
((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
}
}
You are familiar with it. findViewById is done here. Let's use it!
package com.namcooper.apt.androidapt;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import com.namcooper.BindField;
public class MainActivity extends Activity {
@BindField(R.id.xxx)
TextView demoX;
@BindField(R.id.ddd)
TextView demoD;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainActivity_Binder binder = new MainActivity_Binder();
binder.findView(this);
demoX.setText("The test was successful!");
demoD.setText("This is the second control.");
}
}
Operation results:
Successfully avoided frequent findViewById operations! But the problem is also obvious, that is, the user needs to know your naming logic before it can be invoked, this is too Low! ButterKnife just provides a. bind method to do this, so we need to continue optimizing.
+ Customize calling Api
- Create a java library "compiler" and note that the Android library does not need to change the content of build.gradle. Once created successfully, add it to the dependency of app.
- Create Binder Interface Standby
package com.namcooper.apt.bind_api;
public interface Binder {
void findView(Object activity);
}
Here you need to uncomment the process method above for adding interfaces
//To create a class, you should pay attention to the custom naming rules, and unify the "original class name _Binder" with the subsequent use.
TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(ClassName.get("com.namcooper.apt.bind_api","Binder"))//To add the interface implemented by this class, you need to specify the package name and the interface name
.addMethod(findView)//Write the method just generated
.build();
Then let's look at the effect of MainActivity_Binder on the Rebuild project
package com.namcooper.apt.androidapt;
import com.namcooper.apt.bind_api.Binder;
import java.lang.Object;
public final class MainActivity_Binder implements Binder {
public void findView(Object activity) {
((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
}
}
This self-generated class implements the Binder interface and findView method.
- Create Exposed API Build Tool
package com.namcooper.apt.bind_api;
import android.app.Activity;
public class BindTool {
public static void bind(Activity activity) {
//Get the name of the binder
String className = activity.getClass().getName() + "_Binder";
try {
//Obtaining automatically generated classes and objects by reflection
Class binder = Class.forName(className);
Binder b = (Binder) binder.newInstance();
//Call method
b.findView(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
</pre>
//Then go to the project to use it!
```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindTool.bind(this);
demoX.setText("The test was successful!");
demoD.setText("This is the second control.");
}
<div class="se-preview-section-delimiter"></div>
The results are the same as the one above! In this way, we have basically implemented ButterKnife's findViewById function!
- Optimize with ButterKnife Principle
public class BindTool {
//Initialize a collection with the bound activity as key and the corresponding Binder as Value
private static Map<String, Binder> map = new HashMap<>();
public static void bind(Activity activity) {
//Get the name of the binder
String className = activity.getClass().getName() + "_Binder";
//First try to get Binder in the collection, but not through reflection. This ensures that each bound object triggers only one reflection during the program's run, thereby improving performance.
Binder b = map.get(className);
if (b == null)
try {
Class binder = Class.forName(className);
b = (Binder) binder.newInstance();
b.findView(activity);
map.put(className, b);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
<div class="se-preview-section-delimiter"></div>
3. Advanced Function
Of course, ButterKnife also has convenience features such as onclick, bindLaypout, bindfragment, and so on.
These functions seem complex, but in fact they are consistent with the principle of bindView. Here is a simple implementation of onClick.
- Create another annotation class
package com.namcooper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface BindClick {
int value();
}
<div class="se-preview-section-delimiter"></div>
- Add logic to the processor: Logic is no longer interpreted and understandable
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
/**Processing field annotations**/
Set<? extends Element> elements = environment.getElementsAnnotatedWith(BindField.class);
MethodSpec.Builder findViewBuilder = MethodSpec.methodBuilder("findView")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(Object.class, "activity");
String packageName = "";
String className = "";
for (Element el : elements) {
PackageElement packageElement = getPackage(el);
packageName = packageElement.getQualifiedName().toString();
TypeElement typeElement = getClass(el);
className = typeElement.getSimpleName().toString();
TypeMirror mirror = el.asType();
String parameterName = el.getSimpleName().toString();
BindField bindView = el.getAnnotation(BindField.class);
findViewBuilder.addStatement("(($N)activity).$N = ($N)((android.app.Activity)activity).findViewById($L)", className, parameterName, mirror.toString(), bindView.value());
}
MethodSpec findView = findViewBuilder.build();
/**Processing click annotations**/
Set<? extends Element> elementsClick = environment.getElementsAnnotatedWith(BindClick.class);
FieldSpec activity = FieldSpec.builder(TypeName.OBJECT,"activity").build();
MethodSpec.Builder clickOverWrite = MethodSpec.methodBuilder("onClick")
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get("android.view", "View"), "v")
.returns(void.class);
MethodSpec.Builder clickBuilder = MethodSpec.methodBuilder("click")
.addModifiers(Modifier.PUBLIC)
.addParameter(Object.class, "activity")
.returns(void.class);
clickBuilder.addStatement("this.activity = activity");
CodeBlock.Builder switchBlock = CodeBlock.builder().add("switch(v.getId()){\n");
for (Element el : elementsClick) {
PackageElement packageElement = getPackage(el);
packageName = packageElement.getQualifiedName().toString();
TypeElement typeElement = getClass(el);
className = typeElement.getSimpleName().toString();
String methodName = el.getSimpleName().toString();
BindClick bindClick = el.getAnnotation(BindClick.class);
clickBuilder.addStatement("((android.app.Activity)activity).findViewById($L).setOnClickListener(this)", bindClick.value());
switchBlock.add("case $L:\n", bindClick.value());
switchBlock.add("(($N)(activity)).$N();\n",className,methodName);
switchBlock.add("break;\n");
}
switchBlock.add("\n}");
clickOverWrite.addCode(switchBlock.build());
MethodSpec mClickOverWrite = clickOverWrite.build();
MethodSpec mClick = clickBuilder.build();
TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(ClassName.get("com.namcooper.apt.bind_api", "Binder"))
.addSuperinterface(ClassName.get("android.view", "View.OnClickListener"))
.addField(activity)
.addMethod(findView)
.addMethod(mClick)
.addMethod(mClickOverWrite)
.build();
if (packageName.equals("") || className.equals(""))
return true;
JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
.build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
<div class="se-preview-section-delimiter"></div>
- Modification of Binder
public interface Binder {
void findView(Object activity);
void click(Object activity);
}
<div class="se-preview-section-delimiter"></div>
- Modify BindTool
public class BindTool {
private static Map<String, Binder> map = new HashMap<>();
public static void bind(Activity activity) {
//Get the name of the binder
String className = activity.getClass().getName() + "_Binder";
Binder b = map.get(className);
if (b == null)
try {
Class binder = Class.forName(className);
b = (Binder) binder.newInstance();
b.findView(activity);
b.click(activity);
map.put(className, b);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
<div class="se-preview-section-delimiter"></div>
- Call
public class MainActivity extends Activity {
@BindField(R.id.xxx)
TextView demoX;
@BindField(R.id.ddd)
TextView demoD;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
demoX.setText("The test was successful!");
demoD.setText("This is the second control.");
}
@BindClick(value = R.id.xxx)
public void xxxClick() {
Toast.makeText(MainActivity.this, "xxx Click", Toast.LENGTH_SHORT).show();
}
@BindClick(value = R.id.ddd)
public void dddClick() {
Toast.makeText(MainActivity.this, "ddd Click", Toast.LENGTH_SHORT).show();
}
}
<div class="se-preview-section-delimiter"></div>
- rebuild post-generation
public final class MainActivity_Binder implements Binder, View.OnClickListener {
Object activity;
public void findView(Object activity) {
((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
}
public void click(Object activity) {
this.activity = activity;
((android.app.Activity)activity).findViewById(2131427422).setOnClickListener(this);
((android.app.Activity)activity).findViewById(2131427423).setOnClickListener(this);
}
public void onClick(View v) {
switch(v.getId()){
case 2131427422:
((MainActivity)(activity)).xxxClick();
break;
case 2131427423:
((MainActivity)(activity)).dddClick();
break;
}}
}
Don't you think so?
III. Concluding remarks
This article demonstrates the principles of ButterKnife's bindView and BindClick, and the other functions are similar, so the reader can practice them on his own.
Of course, compared with ButterKnife, the example in this article is very simple. And in Android Studio, there are plug-ins specifically developed for ButterKnife that can generate annotations quickly, thus greatly facilitating users.
This article is only for learning and communication. Welcome to make corrections! ____________