0, probe,

We programmers are more or less lazy. We’re always trying to get our computers to do things for us, so we’ve learned all kinds of languages to tell computers what to do — even though they sometimes misunderstand us.

One day, I realized that some code could actually be generated according to certain rules, but you couldn’t just write it — not all duplicate code can be eliminated by refactoring and using high-end techniques like generics — like the code I hate the most:

TextView textView = (TextView) findViewById(R.id.text_view);
Button button = (Button) findViewById(R.id.button);
Copy the code

You can’t stop writing code like that, it’s frustrating. Suddenly thought of the story of back words before: back but C, back but V. Well, maybe I’ll write Android app but findViewById as well.

ButterKnife is a framework for assisted code generation that relies on Java’s annotation mechanism. After reading this article, you will understand the power of Java’s annotation processor. You’ll also have some awareness of frameworks like Dagger2 and AndroidAnnotations.

1. First acquaintance with ButterKnife

1.1 ButterKnife profile

To be honest, I’ve always had a problem with findViewById, but then I saw Afinal, so we could just annotate it and say, wow, “Byte Byte” to findViewById.

What? Village see is not medium write yao?

After all, it’s mobile, and there’s always a visceral resistance to frameworks like Afinal that use reflection to implement injection, so…

Don’t cry, you don’t need to reflect

There’s an amazing company called Square, and Jake Wharton has opened source a fantastic framework called ButterKnife, which uses annotations for injection, but it generates code at compile time and has no side effects at runtime. It works really fast. The effect is good, but the compilation time has a little time cost.

As an aside, if YOU don’t know Jake Wharton on Android these days, I think you can Pass the interview. Ha ha, just kidding

1.2 How do I use a ButterKnife?

How to introduce something is really a problem of learning. Don’t keep saying I’m uneducated. I mean, it’s a bit convoluted.

Let’s start with a brief introduction to the basic usage of ButterKnife, which you can also see in ButterKnife.

Simply put, there are three steps to using a ButterKnife:

  1. Configuring the build environment is a bit more configuration than a normal framework because Butterknife uses an annotation processor, but it’s simple:

    Dependencies buildscript {repositories {mavenCentral ()} {the classpath 'com. Android. View the build: gradle: 1.3.1' classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'}} the apply plugin: 'com. Neenbedankt. Android - apt... Dependencies {the compile 'com. Jakewharton: butterknife: 8.1.0 apt' com. Jakewharton: butterknife - compiler: 8.1.0 '}Copy the code
  2. Annotate objects with annotations, such as views, and some event methods (for onClick and so on), for example:

    @Bind(R.id.title) TextView title; @OnClick(R.id.hello) void sayHello() { Toast.makeText(this, "Hello, views!" , LENGTH_SHORT).show(); ButterKnife.apply(headerViews, ALPHA_FADE); }Copy the code
  3. After initializing the layout, call the bind method:

    setContentView(R.layout.activity_main); ButterKnife.bind(this); // The setContentView must come after the setContentView, otherwise you will be playing with null PointersCopy the code

FindViewById, setOnClickListener, findViewById, setOnClickListener

Ha, but be careful. If you can write like this, ButterKnife says, “I’ll show you a mistake!”

@Bind(R.id.title) private TextView title; @OnClick(R.id.hello) private void sayHello() { Toast.makeText(this, "Hello, views!" , LENGTH_SHORT).show(); ButterKnife.apply(headerViews, ALPHA_FADE); }Copy the code
Error:(48, 22) error: @Bind fields must not be private or static. (com.example.butterknife.SimpleActivity.title)
Error:(68, 18) error: @OnClick methods must not be private or static. (com.example.butterknife.SimpleActivity.sayHello)
Copy the code

Why is that? If you know the mechanics of ButterKnife, this is clear. As we mentioned earlier, ButterKnife achieves its injection purposes by generating auxiliary code through the annotation processor, so it’s worth taking a look at what the heck it generates.

Words, the generated code is in the build/generated/source/apt here, we’ll ButterKnife official sample, for example, the generated code is as follows:

Let’s take a look at SimpleActivity? ViewBinder:

public class SimpleActivity$ViewBinder implements ViewBinder { @Override public void bind(final Finder finder, final T target, Object source) { Unbinder unbinder = new Unbinder(target); View view; FindRequiredView (source, 2130968576, "field 'title'"); target.title = finder.castView(view, 2130968576, "field 'title'"); // Insert hello Button View = finder.findRequiredView(source, 2130968578, "Field 'hello', method 'sayHello', and method 'sayGetOffMe'"); target.hello = finder.castView(view, 2130968578, "field 'hello'"); unbinder.view2130968578 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.sayHello(); }}); . }... }Copy the code

We see that there is a method called bind, which bears an obvious relation to the butterknife. bind we called earlier — in fact, the latter is just an introduction to call the generated code. What, no? Well, I love you curious kids. After calling butterknife. bind, we enter the following method:

static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) { Class targetClass = target.getClass(); try { ViewBinder viewBinder = findViewBinderForClass(targetClass); viewBinder.bind(finder, target, source); } catch (Exception e) {// omit Exception}}

ViewBinder (target) and Source (Source) are both instances of our Activity.

private static ViewBinder findViewBinderForClass(Class cls) throws IllegalAccessException, InstantiationException { ViewBinder viewBinder = BINDERS.get(cls); If (viewBinder! = null) { return viewBinder; } String clsName = cls.getName(); if (clsName.startsWith("android.") || clsName.startsWith("java.")) { return NOP_VIEW_BINDER; } try {// Find the Activity Class name plus "$ViewBinder" Class, instantiate, and return Class viewBindingClass = class.forname (clsName + "$ViewBinder"); //noinspection unchecked viewBinder = (ViewBinder) viewBindingClass.newInstance(); } catch (ClassNotFoundException e) {viewBinder = findViewBinderForClass(cls.getSuperClass); } // cache viewBinder binders. put(CLS, viewBinder); return viewBinder; }

If the Activity name is SimpleActivity, the ViewBinder should be SimpleActivity. ViewBinder.

If the member to be injected is private, ButterKnife will report an error. If the title is private, the generated code will write target.title. Kid, you think just because you're generating code, the Java virtual machine will show you things you shouldn't?

Of course, there are more requirements for members that need to be injected. As we will see later, injection is also not supported for static members and members of certain classes in certain packages.

1.3 summary

This frame gives us the impression that it's easy to cook chicken. Back in the day, the @ sign gave us the feeling of surfing the Internet. Now, we still only need a few @ clicks in the code, and can be behind the various waves.

Wait, what is the secret behind such a simple appearance? Beneath its bright exterior there are those unspeakable stories? See the next breakdown.

Please bring me a plate of fried rice with egg

Jake, I bet you a month Hollywood member that you're a foodie.

We compare the process of generating code to an egg fried rice, in which you prepare the cooker first, then prepare the ingredients, then stir fry and serve.

2.1 Prepare cooking utensils

Egg fried rice is cooked in a wok, so what's with our ButterKnife "wok"?

Without further ado, what is the process from our configured annotations to the resulting code?

The picture above is clear, even though it doesn't say anything. The forehead.. Don't touch me.

If you look at ButterKnife in this picture, she's a bully. By whose power? We've been waiting a long time for the drop annotation processor to come out, and this is the stage of history!

The Java compiler preprocesses the code before it compiles it, and when the compiler calls the annotation handler on the classpath with the configuration shown below, we can do our dirty work.

So, if you want to write your own annotation processor, first inherit AbstractProcessor and then write a similar configuration. But wait a minute, let's see how Butterknife does it:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
    ...
}
Copy the code

What does an AutoService do? Look at the picture. Did you notice that folder is red? Yes, it is automatically generated, and the guy responsible for generating this configuration is AutoService, which is an open source component from Google and is very simple, so I won't go into details.

In short: The annotation processor opens the door for us to execute a piece of our code before the Java compiler compiles it. Of course, this code does not have to generate other code, you can check that the code annotations are properly named (zhou Zhiming's book "Understanding the Java Virtual Machine" has this example). Ah, you say you are going to output a "Hello World", and then ╯ to pass pass. ..

2.2 Hey egg fried rice, the easiest and most difficult

Now that we know the entry point, it's time to take a look at ButterKnife's dirty tricks. In this case, all the input is the annotations we configured in our own code, and all the output is the generated auxiliary code to inject the object.

About annotation processor for more details please refer to the corresponding data, I here give ButterKnife core code, directly in ButterKnifeProcessor. In the process:

@Override public boolean process(Set elements, RoundEnvironment env) {Map targetClassMap = findAndParseTargets(env); // After parsing is complete, the code structure that needs to be generated is present in each BindingClass. For (map.entry Entry: targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingClass bindingClass = entry.getValue(); // This step completes the actual code generation bindingClass.brewJava().writeto (filer); } catch (IOException e) { error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage()); } } return true; }Copy the code

As we know, ButterKnife has requirements for the members that need to be injected into the object. When parsing the annotation configuration, ButterKnife first checks the annotated members. If the check fails, an exception is thrown.

In parsing, we use @bind as an example. The annotation handler finds the members labeled @bind, checks if they meet the conditions for injection (e.g., they cannot be private, they cannot be static, etc.), and extracts the values from the annotation. Create or update the corresponding BindingClass.

private Map findAndParseTargets(RoundEnvironment env) { Map targetClassMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element. for (Element element : env.getElementsAnnotatedWith(Bind.class)) { if (! SuperficialValidation.validateElement(element)) continue; try { parseBind(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, Bind.class, e); }}... return targetClassMap; }Copy the code

Now take the title variable as an example. The element that you get when parsing is actually the title variable.

private void parseBind(Element element, Map targetClassMap, Set erasedTargetNames) { ... Omit the code to verify that Element meets the criteria... TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.ARRAY) { parseBindMany(element, targetClassMap, erasedTargetNames); } else if (LIST_TYPE.equals(doubleErasure(elementType))) { parseBindMany(element, targetClassMap, erasedTargetNames); } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {if (isSubtypeOfType(elementType, ITERABLE_TYPE)) { Error (element, "@%s must be a List or array. (%s.%s)", bind.class.getSimplename (), ((TypeElement) element.getEnclosingElement()).getQualifiedName(), element.getSimpleName()); } else { parseBindOne(element, targetClassMap, erasedTargetNames); }}Copy the code

When the title is injected, the corresponding parseBindOne method is then executed:

private void parseBindOne(Element element, Map targetClassMap, Set erasedTargetNames) { ... Omit some verification code... if (! isSubtypeOfType(elementType, VIEW_TYPE) && ! isInterface(elementType)) { ... Handler error, apparently injected must be a subclass of View... } // Assemble information on the field. int[] ids = element.getAnnotation(Bind.class).value(); if (ids.length ! = 1) {... We have confirmed that it is a single value binding, so we can get an error if there are more than one parameter. }... Omit the code that builds the BindingClass object... BindingClass bindingClass = targetClassMap.get(enclosingElement); String name = element.getSimpleName().toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); FieldViewBinding binding = new FieldViewBinding(name, type, required); bindingClass.addField(id, binding); . }Copy the code

In fact, each annotation parsing process is similar, the ultimate goal of parsing is to addField in the bindingClass, what does that mean?

From the previous analysis, we already know that the ultimate goal of parsing annotations is to generate the code for injection, just as we let the annotation manager write the code. This seems like an interesting topic, but if your program is smart enough, it can write its own code

So addField just adds an attribute to the generated code? No, no, no, it's adding a set of injection relationships that the annotation manager will need to parse to organize the generated code when it's generated. So, do you want to take another look at the generated code and see what else you can find?

2.3. Out of the pot

Now that the annotation configuration has been parsed and we know what the code we're generating looks like, the next question is how to actually generate the code. Here we use a tool called JavaPoet, also from Square. JavaPoet provides very powerful code generation capabilities, such as the code that generates JavaDemo output HelloWorld below:

MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!" ) .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out);Copy the code

This generates the following code:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}
Copy the code

It's not too hard to write a program to generate some code, but it can be very frustrating to guide a package. Don't worry, JavaPoet can take care of it all.

With that in mind, let's take a look at what ButterKnife does with JavaPoet. Remember the following code:

bindingClass.brewJava().writeTo(filer);
Copy the code

This code writes whatever the hell comes out of brew into a filer, which is something like a file. In other words, this code completes the process of generating code into a specified file.

Brew Java !!
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[ _ ]P coffee! [ _ ]P

JavaFile brewJava() {TypeSpec.Builder result = TypeSpec. ClassBuilder (className) // Add modifier to public, The generated class is public.addModiFIERS (public).addTypeVariable(typeVariablename.get ("T", className.Bestguess (targetClass))); /* If (parentViewBinder!) if (parentViewBinder!) if (parentViewBinder! = null) { result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder), TypeVariableName.get("T"))); } else { result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T"))); } if (hasUnbinder()) { result.addType(createUnbinderClass()); } result.addmethod (createBindMethod()); // Output a JavaFile object (actually this is very close to generating the final code), Complete the return JavaFile. Builder (classPackage, result.build()) .addFileComment("Generated code from Butter Knife. Do not modify!" ) .build(); }Copy the code

Now we need to move on to the createBindMethod method, which is the key to generating code

Private MethodSpec createBindMethod() {/* Create a method called bind with @override annotation. Method visibility is public and some parameter types */ methodSpec.builder Result = methodSpec.methodBuilder ("bind").addAnnotation(Override. Class) .addModifiers(PUBLIC) .addParameter(FINDER, "finder", FINAL) .addParameter(TypeVariableName.get("T"), "target", FINAL) .addParameter(Object.class, "source"); if (hasResourceBindings()) { // Aapt can change IDs out from underneath us, just suppress since all will work at runtime. result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType") .build()); } // Emit a call to the superclass binder, if any. if (parentViewBinder ! = null) { result.addStatement("super.bind(finder, target, source)"); } /* We never mentioned unbinder if we had the following injection configuration: @unbinder ButterKnife.Unbinder unbinder; * Add the following code to the generated code, */ / If the caller requester an unbinder, we need to create an instance of it. if (hasUnbinder()) { result.addStatement("$T unbinder = new $T($N)", unbinderBinding.getUnbinderClassName(), unbinderBinding.getUnbinderClassName(), "target"); TextView = (TextView) findViewById(...) */ if (! viewIdMap.isEmpty() || ! collectionBindings.isEmpty()) { // Local variable in which all views will be temporarily stored. result.addStatement("$T  view", VIEW); // Loop over each view bindings and emit it. for (ViewBindings bindings : viewIdMap.values()) { addViewBindings(result, bindings); } // Loop over each collection binding and emit it. for (Map.Entry entry : collectionBindings.entrySet()) { emitCollectionBinding(result, entry.getKey(), entry.getValue()); Bind unbinder if was requested. If (hasUnbinder()) {result.addStatement("target unbinder", unbinderBinding.getUnbinderFieldName()); } /* ButterKnife supports not only views but also strings, themes, and images. */ If (hasResourceBindings()) {// There's no room for that, I'll leave them out... } return result.build(); }Copy the code

For some reason, this code reminds me of how I write code. Then ButterKnife is clearly writing code for us.

Of course, this is only the most important and core part of the generated code. To make it easier to understand, I have listed the method generated in the demo for easy viewing:

@Override public void bind(final Finder finder, final T target, Object source) {// Construct unbinder unbinder = new unbinder (target); // Insert a view view; view = finder.findRequiredView(source, 2130968576, "field 'title'"); target.title = finder.castView(view, 2130968576, "field 'title'"); / /... Omitting the injection of other members... Unbinder target. Unbinder = unbinder; }Copy the code

Hack and define our own annotation BindLayout

I always thought that since the View can be injected, can we also inject layout? Obviously it's not that hard, but why didn't Jake do it? I think it's mostly because... If you want to inject a layout, you should write something like this

@BindLayout(R.layout.main) public class AnyActivity extends Activity{... }Copy the code

But how do we usually write?

public class AnyActivity extends Activity{ @Override protected void onCreate(Bundle savedInstances){ super.onCreate(savedInstances); setContentView(R.layout.main); }}Copy the code

You don't say you don't inherit the onCreate method, so you always have to say it's not cost-effective, right? Who knows...

However,, let's apply our magic skills to ButterKnife. Well, he's talking about social crab master @meaning), allowing ButterKnife to @bindLayout. First look at the effect:

// Insert layout @bindLayout (r.layout.simple_activity) public class SimpleActivity extends Activity {... }Copy the code

Generated code:

public class SimpleActivity$ViewBinder implements ViewBinder { @Override public void bind(final Finder finder, Final T target, Object source) {// Generate this code to inject layout Target.setContentView (2130837504); // Add unbinder, view, and view. }... }Copy the code

So how do we do that? In a word, follow the trail

The first step, of course, is to define the annotation BindLayout

@Retention(CLASS) @Target(TYPE)
public @interface BindLayout {
    @LayoutRes int value();
}
Copy the code

The second step is to add support for this annotation in the annotation handler:

@Override public Set getSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); . types.add(BindLayout.class.getCanonicalName()); . return types; }Copy the code

Third, support should be added to the parsing part of the annotation handler:

private Map findAndParseTargets(RoundEnvironment env) { Map targetClassMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element. for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) { if (! SuperficialValidation.validateElement(element)) continue; try { parseBindLayout(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindLayout.class, e); }}... }Copy the code

Here is the parseBindLayout method:

Private void parseBindLayout(Element Element, Map targetClassMap, Set erasedTargetNames) {/* Unlike other annotations, */ TypeElement TypeElement = (TypeElement) Element; Set modifiers = element.getModifiers(); // Only private is not accessible, static is not affected, Error (Element, "@%s %s must not be PRIVATE. (%s.%s)", BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(), element.getSimpleName()); return; } / / in the same way, the android package of the beginning of the class does not support the String qualifiedName. = typeElement getQualifiedName (), toString (); if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", BindLayout.class.getSimpleName(), qualifiedName); return; } // Similarly, If (qualifiedName.startswith (" Java.")) {error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", BindLayout.class.getSimpleName(), qualifiedName); return; } /* We only support activities for now. If you want to support fragments, you need to treat them separately. isSubtypeOfType(typeElement.asType(), ACTIVITY_TYPE)){ error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindLayout.class.getSimpleName(), typeElement.getQualifiedName(), element.getSimpleName()); return; } / / get the value of the incoming annotations such as R.l ayout. Main int layoutId = typeElement. GetAnnotation (BindLayout. Class). The value (); if(layoutId == 0){ error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)", BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(), element.getSimpleName()); return; } BindingClass bindingClass = targetClassMap.get(typeElement); if (bindingClass == null) { bindingClass = getOrCreateTargetClass(targetClassMap, typeElement); } / / put the value of the layout to the bindingClass, here I just simple saved. Under this value bindingClass setContentLayoutId (layoutId); log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames); }Copy the code

The fourth step, add the corresponding support, code generation in the BindingClass. CreateBindMethod:

private MethodSpec createBindMethod() { MethodSpec.Builder result = MethodSpec.methodBuilder("bind") .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(FINDER, "finder", FINAL) .addParameter(TypeVariableName.get("T"), "target", FINAL) .addParameter(Object.class, "source"); if (hasResourceBindings()) { ... Omit the... } // If layoutId is not set to 0, then there is a binding. Add setContentView. If (layoutId! = 0){ result.addStatement("target.setContentView($L)", layoutId); }... }Copy the code

So we can say goodbye to the setContentView and make a comment, very refreshing, and just open the Activity at random and see where the layout is, hahahahahaha

It actually means you are fat.

Androidannotations and dagger2

4.1 androidannotations

Androidannotations are also an injection tool, and if you take a little bit of interest in it, you'll see that it works in much the same way as ButterKnife. Here is the very core of the code:

	private void processThrowing(Set annotations, RoundEnvironment roundEnv) throws Exception {
		if (nothingToDo(annotations, roundEnv)) {
			return;
		}

		AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
		AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
		androidAnnotationsEnv.setValidatedElements(validatingHolder);

		try {
			AndroidManifest androidManifest = extractAndroidManifest();
			LOGGER.info("AndroidManifest.xml found: {}", androidManifest);

			IRClass rClass = findRClasses(androidManifest);

			androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest);

		} catch (Exception e) {
			return;
		}

		AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);

		ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);

		generateSources(processResult);
	}
Copy the code

Annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations annotations I suggest you pay more attention to its code structure design, very good.

From the perspective of use, ButterKnife is UI injection, and androidAnnotations are relatively simple, whereas AndroidAnnotations are a bit large and powerful. Which framework to use depends on specific requirements.

4.2 Dagger 2

Dagger 2 is a super rich kid with a Square mom and A Google dad -- Dagger 2 originated from Square's open source project and has now been taken over by Google.

Dagger is a Dagger, and it's also a framework for injecting members, but as opposed to the previous two frameworks, it's

  • It's more basic because it's not business specific
  • It is more generic because it is not platform dependent
  • It is more complex because it focuses more on dependencies between objects

One day, we realized that our constructor needed 3000 lines, and we realized that it was time to write a framework to help us complete the constructor.

In other words, if your constructor is not that long, there is no need to introduce Dagger 2 because that will make your code look like... It's not that easy to understand.

Of course, WE put the Dagger 2 here because it completely reflects, and the idea of implementing it is exactly the same as the previous two frameworks. So you can say without thinking, Dagger 2 must have at least two modules, one is compiler with annotation processor inside; There is also a module that the runtime depends on, which provides annotation support for the Dagger 2, etc.

5, summary

In this paper, through the analysis of ButterKnife source code, we understand the implementation principle of ButterKnife such an injection framework, and we also have a certain understanding of Java annotation processing mechanism; We then tried a simple extension of ButterKnife -- all in all, the implementation of the ButterKnife framework, which is very simple to use, actually covers a lot of knowledge, which is relatively obscure, but very powerful, and we can use these features to implement a variety of personalized needs. Let our work efficiency further improve.

Come, free our hands!

Copy the codeCopy the code