The ButterKnife plugin frees Android programmers from repetitive findViewById code, especially with Android Studio plug-ins that automatically generate code. AbstractProcess is used to automatically generate findViewById code for bindView-annotated controls at compile time. The ButterKnife#bind(Activity) method, It’s essentially calling this auto-generated findViewById code. However, when I needed to understand the implementation details, I decided to look at the source code for ButterKnife. The whole project of ButterKnife covers a lot of annotations, which may seem to consume a lot of time. Based on the ideas of the project explored these days, the author realized the use of his own BindView annotations to help you understand.

This article realizes the function

FindViewById is implemented in the Activity using the @bindView annotation.

Making a link

The author’s project has been uploaded to Github, welcome everyone star. Click to see ButterFork

The project structure

Annotation module

The BindView annotations we need to work with are declared in this Module.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value(a) ;
}Copy the code

Target is of type FIELD, indicating that the annotation is used to declare an attribute within the class. Retention = CLASS; Retention = CLASS; Retention = RUNTIME; RUNTIME: RUNTIME; RUNTIME: RUNTIME; RUNTIME: RUNTIME; RUNTIME: RUNTIME; RUNTIME: RUNTIME; The BindView value is of type int, which is the same type as r.id.

@BindView(R.id.btn)
protected Button mBtn;Copy the code

Compiler module

This Module is the focus of automatically generating findViewById code, where there is only one class, derived from AbstractProcessor.

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindProcess extends AbstractProcessor{
    private Elements mElementsUtil;

    /** * key: eclosed elemnt * value: inner views with BindView annotation */
    private Map<TypeElement,Set<Element>> mElems;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementsUtil = processingEnv.getElementUtils();
        mElems = new HashMap<>();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> types = new HashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Process start !");

        initBindElems(roundEnv.getElementsAnnotatedWith(BindView.class));
        generateJavaClass();

        System.out.println("Process finish !");
        return true;
    }

    private void generateJavaClass(a) {
        for (TypeElement enclosedElem : mElems.keySet()) {
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .addParameter(ClassName.get(enclosedElem.asType()),"activity")
                    .returns(TypeName.VOID);
            for (Element bindElem : mElems.get(enclosedElem)) {
                methodSpecBuilder.addStatement(String.format("activity.%s = (%s)activity.findViewById(%d)",bindElem.getSimpleName(),bindElem.asType(),bindElem.getAnnotation(BindView.class).value()));
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("Bind"+enclosedElem.getSimpleName())
                    .superclass(TypeName.get(enclosedElem.asType()))
                    .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                    .addMethod(methodSpecBuilder.build())
                    .build();
            JavaFile file = JavaFile.builder(getPackageName(enclosedElem),typeSpec).build();
            try {
                file.writeTo(processingEnv.getFiler());
            } catch(IOException e) { e.printStackTrace(); }}}private void initBindElems(Set<? extends Element> bindElems) {
        for (Element bindElem : bindElems) {
            TypeElement enclosedElem = (TypeElement) bindElem.getEnclosingElement();
            Set<Element> elems = mElems.get(enclosedElem);
            if (elems == null){
                elems=  new HashSet<>();
                mElems.put(enclosedElem,elems);
                System.out.println("Add enclose elem "+enclosedElem.getSimpleName());
            }
            elems.add(bindElem);
            System.out.println("Add bind elem "+bindElem.getSimpleName()); }}private String getPackageName(TypeElement type) {
        returnmElementsUtil.getPackageOf(type).getQualifiedName().toString(); }}Copy the code

The @autoservic class annotation is used to automatically generate meta-INF information. For classes that inherit from AbstractProcessor, they need to be declared in meta-INF to take effect at compile time. With AutoService, annotated classes can be added to meta-INF automatically. To use AutoService, import the following packages:

compile 'com. Google. Auto. Services: auto - service: 1.0 rc2'Copy the code

Will be executed and then compile time proces, code generation method parameter annotautions is a collection, due to the above getSupportedAnnotationTypes returns the @ BindView annotations, So the Annotations parameter contains all the elements annotated by @BindView. The generateJavaClass method uses the map to generate code, which uses classes in the Javapoet package to easily generateJava classes, methods, modifiers, and so on. The method body class code may seem complicated, but with a little learning about the Javapoet package, you can quickly become familiar with the function of this method. Here is the generated Java class code:


public final class BindMainActivity extends MainActivity {
  public static void bind(MainActivity activity) {
    activity.mBtn = (android.widget.Button)activity.findViewById(2131427422);
    activity.mTextView = (android.widget.TextView)activity.findViewById(2131427423); }}Copy the code

The annotated original class is as follows:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn)
    protected Button mBtn;

    @BindView(R.id.text)
    protected TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindMainActivity.bind(this)}}Copy the code

The generated Java class is located as follows:

mybutterknife module

In MainActivity’s onCreat method, after calling setContentView, Can directly call BindMainActivity. BindView (this) to complete each View and the id of the binding and instantiation. But if we look at the implementation in ButterKnife, no matter which Activity class it is, it is injected by calling the Butterknife.bindView (this) method. In the project code, different class, derived class will generate different name, for example, if there is another HomeActivity class, the injection should be used BindHomeActivity. BindView (this). How do you implement a unified approach to injection like ButterKnife? Looking at the source code again, you can see that the Butterknife. bindView method still uses reflection to call methods in the generated class, that is, butterknife. bindView simply provides a unified entry point. By contrast, in myButterKnife Module, we can use reflection to implement similar method routing to unify all injection method entries:

public class ButterFork {
    private static Map<Class,Method> classMethodMap = new HashMap<>();
    public static void bind(Activity target){
        if(target ! =null){
            Method method = classMethodMap.get(target.getClass());
            try {
                if (method == null) {
                    String bindClassName = target.getClass().getPackage().getName() + ".Bind" + target.getClass().getSimpleName();
                    Class bindClass = Class.forName(bindClassName);
                    method = bindClass.getMethod("bind", target.getClass());
                    classMethodMap.put(target.getClass(), method);
                }
                method.invoke(null, target);
            }catch(Exception e){ e.printStackTrace(); }}}}Copy the code

sample module

To sum up, we have easily implemented our own BindView annotations as follows:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn)
    protected Button mBtn;

    @BindView(R.id.text)
    protected TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyButterKnife.bind(this);
        mBtn.setText("changed");
        mTextView.setText("changed too"); }}Copy the code

Run the code, perfect!