The application of ButterKnife

As shown in the question, this article mainly discusses how to write a simple version of ButterKnife by hand. Therefore, I will first take a look at how to use the ButterKnife, but most people have used the ButterKnife, so I will not go into details. If you need to refer to:

Github.com/JakeWharton…

Although some people feel that ButterKnife has become so rarely used in Kotlin using KTX tools or MVVM that it is not necessary to study ButterKnife, we learn it not to use it, but to understand its principle and construction, as in this article, Just use handwritten ButterKnife to learn how to implement annotations, annotationProcessor, etc. Drunk weng’s meaning is not wine, care about the mountains and rivers.

All right, without further ado, here’s my show:

Add dependencies to build.gradle:

android { ... // Butterknife requires Java 8. compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion. Dependencies VERSION_1_8}} {implementation 'com. Jakewharton: butterknife: 10.2.3 annotationProcessor 'com. Jakewharton: butterknife - compiler: 10.2.3'}Copy the code

Write in MainActivity:

public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_content) TextView tvContent; @override protected void onCreate(Bundle savedInstanceState) {··· butterknife. bind(this); Tvcontent.settext (" Changed successfully!" ); }}Copy the code

em… Run result does not send, anyway it is successful 😂.

Breaking it down a bit, ButterKnife’s code is used in two steps:

  • @BindView(R.id.tv_content): Note statement
  • ButterKnife.bind(this);Note:

Annotation statement

First, we create a new annotation mybindView.java:

public @interface MyBindView {
}
Copy the code

A comment is a label statement that has no actual special code effect, just like a comment:

  • Single-line comment // This is a single-line comment
  • Multi-line comment /This is a multi-line comment/
  • Javadoc comments / *This is a Javadoc comment/

But instead of annotations being erased at the bytecode stage, annotations can be retained at any stage and come with other features:

  • @Retention
    • Retentionpolicy. SOURCE: only in the SOURCE phase, other phases are erased
    • Retentionpolica. CLASS: reserved to the bytecode phase, and subsequent phases are erased
    • Retentionpolicy.runtime: Retained until RUNTIME, i.e. annotations can be read while the code is running
  • @Documented
    • Markup annotations can be included in user documentation
  • @Target
    • Restrict annotable objects, such as member variables, methods, classes, etc., to see the ElementType class for details
  • @Inherited
    • Identifies the annotation as inheritable. If A has @inherited, @Retention, and @target, and B has no annotations, but B extends A, THEN B will inherit the annotations of A

Based on the above instructions, we can improve the annotation.

Since our annotations are used at runtime and only annotate member variables, we can write:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
}
Copy the code

Then it’s ready to use:

    @MyBindView
    TextView tvContent;
Copy the code

However, this is not enough, we do not know which ID to bind to, so we need to add variables to the annotation so that the assignment passes and we know which ID to bind to:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
    int viewId();
}
Copy the code

Huh? Why is there a () after viewId? It’s just a rule of how to write it, so remember that.

Let’s look at using:

    @MyBindView(viewId = R.id.tv_content)
    TextView tvContent;
Copy the code

Assignment successful!

em… There’s an extra viewId, but if you remove it, you’ll get an error, right? Why doesn’t ButterKnife need to write and I do?

If the viewId name is value, the compiler will automatically assign the value, so we need to change it slightly:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
    int value();
}
Copy the code
    @MyBindView(R.id.tv_content)
    TextView tvContent;
Copy the code

Ok, the annotation declaration function is complete. Let’s push it a little harder and get rid of the annotations.

Annotations parsing

ButterKnife starts parsing with the following code:

ButterKnife.bind(this);
Copy the code

Let’s create a new class:

public class MyButterKnife {
    public static void bind(Activity activity){
    }
}
Copy the code

OK, end scatter flower ~~

No, we need to write the control’s binding code inside bind().

First, let’s clarify the logic, as long as the logic is fine, the code implementation is not a problem:

  • Gets all member variables for the Activity
  • Determines whether the member variable is annotated by MyBindView
  • If the annotations match, the findViewById assignment is made to the member variable

The specific code is as follows:

Public static void bind(Activity Activity) {// Get all member variables of the Activity for (Field Field: Activity.getclass ().getDeclaredFields()) {// Check whether this member variable is annotated by MyBindView MyBindView = field.getAnnotation(MyBindView.class); if (myBindView ! = null) {try {// Field = activity.findViewById(myBindView.value())) field.set(activity, activity.findViewById(myBindView.value())); } catch (IllegalAccessException e) { e.printStackTrace(); }}}}Copy the code

Run!

I looked at the phone and it worked fine!

Although, the above function implementation is no problem, but, each control binding depends on reflection, which is too performance, a good, but normal activities are more than one View, with the increase of View, execution time is longer, so, we must find a new way, That’s the AnnotationProcessor.

AnnotationProcessor preface

Before we talk about AnnotationProcessor, let’s think about how we can batch bind views without consuming too much performance.

em…

The least costly way to bind a View is to use findViewById directly. If so, is there a way to generate findViewById code at compile time and then call it when you use it?

That seems like a good idea, so let’s take a look at what the code that generates a good findViewById looks like.

Simulation generates good code styles:

public class MyButterKnife { public static void bind(MainActivity activity) { activity.tvContent = activity.findViewById(R.id.tv_content); }}Copy the code

That’s a problem, though, because the bind() method is supposed to pass in an Activity, and we’re not going to call it MainActivity, and we’re going to have to decide which class to load the findViewById code based on what the Activity is. So we need to create a new class. This class name has a fixed form, which is the original Activity name +Binding:

Simulate automatically generated files:

public class MainActivityBinding { public MainActivityBinding(MainActivity activity) { activity.tvContent = activity.findViewById(R.id.tv_content); }}Copy the code

Modified MyButterKnife

Public class MyButterKnife {public static void bind(Activity Activity) {try {// Get the "current Activity class name +Binding" class object Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding"); // Get the constructor of the class object, The construction method of parameters for the current activity object Constructor Constructor. = bindingClass getDeclaredConstructor (activity. The getClass ()); // Call constructor. NewInstance (activity); } catch (Exception e) { e.printStackTrace(); }}}Copy the code

Run!

I looked at the phone, it still works!

So, we only have one problem left, which is how to dynamically generate the MainActivityBinding class.

This is where the AnnotationProcessor is really needed.

AnnotationProcessor is a tool that processes annotations, finds annotations in source code, and automatically generates code based on annotations.

AnnotationProcessor use

First, we’ll create a new Module.

Android Studio –> File –> New Module –> Java or Kotlin Library –> Next –>

As for the naming, everyone according to their own situation:

Then click the Finish button.

First, let MyBindingProcessor inherit AbstractProcessor,

  • process(): contains code that automatically generates code.
  • getSupportedAnnotationTypes(): Returns the supported annotation type.
public class MyBindingProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } @Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); }}Copy the code

However, in order for MyBindingProcessor to be called automatically, we need to configure a little bit:

Project structure changes

In addition, in order to decouple functions and develop subsequent functions, we need to extract myButterknife. Java and mybindView. Java files separately and store them in one module respectively. The directory structure is as follows:

App depends on:

    implementation project(path: ':my-reflection')
    annotationProcessor project(':my-processor')
Copy the code

My – reflection depends on:

    api project(path: ':my-annotations')
Copy the code

My – processor depends on:

    implementation project(':my-annotations')
Copy the code

All right, we’re all set.

We went back to mybindingProcessor.class, added annotation support and printed logs. After all, we need to test the configuration to see if there are any problems. If there are, we can go back to see if there are any errors.

public class MyBindingProcessor extends AbstractProcessor { @Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {system.out.println (" Configuration successful!" ); return false; } @ Override public Set < String > getSupportedAnnotationTypes () {/ / only supports MyBindView annotations to return Collections.singleton(MyBindView.class.getCanonicalName()); }}Copy the code

Test configuration

Run Terminal and type./gradlew :app:compileDebugJava to check whether the output is configured successfully. If yes, the configuration is successful!!

Code generation

With the new library, there is one more dependency:

Implementation 'com. Squareup: javapoet: 1.12.1'Copy the code

Code flooding in! Alert! Alert!!!!!

But don’t worry, I have annotated everything, it is easy to understand, even if you don’t understand, it doesn’t matter, the main thing is to know the whole process and logic, when you really need to do, you can slowly study, after all, doing a Demo and doing an online project, are two different things.

@Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {extends TypeElement> set, RoundEnvironment RoundEnvironment. GetRootElements ()) {/ / get the package name String packageStr = element. GetEnclosingElement (). The toString (); String classStr = element.getSimplename ().toString(); + Binding ClassName ClassName = classname. get(packageStr, classStr + "Binding"); / / to build the new class constructor MethodSpec. Builder constructorBuilder = MethodSpec. ConstructorBuilder () addModifiers (Modifier. The PUBLIC) .addParameter(ClassName.get(packageStr, classStr), "activity"); Boolean hasBuild = false; For (Element enclosedElement: enclosedElement) Element. GetEnclosedElements ()) {/ / only if get member variables (enclosedElement. GetKind () = = ElementKind. FIELD) {/ / determine whether be MyBindView annotation MyBindView bindView = enclosedElement.getAnnotation(MyBindView.class); if (bindView ! = null) {// Set to generate class hasBuild = true; / / add the code in a constructor constructorBuilder. AddStatement (" activity. $N = activity. The findViewById ($L) ", enclosedElement.getSimpleName(), bindView.value()); If (hasBuild) {try {// build a new class TypeSpec builtClass = TypeSpec. ClassBuilder (className) .addModifiers(Modifier.PUBLIC) .addMethod(constructorBuilder.build()) .build(); Javafile.builder (packageStr, builtClass).build().writeto (filer); } catch (IOException e) { e.printStackTrace(); } } } return false; }Copy the code

The build directory does not have files generated:

The end of the sow flowers, ✿ °, °) Blue ✿

em… It just occurred to me that since the auto-generated code is executed at compile time, annotations don’t need to exist at the bytecode stage after compile, so just keep them at the source stage instead.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyBindView {
    int value();
}
Copy the code

Source code address:

Github.com/bjsdm/MyBut…