Generating code using APT in Android

Keywords: ButterKnife Java Android github

APT is not new anymore, although we all know what it is:
APT (short for Annotation Processing Tool) parses annotations at code compilation time and generates new Java files.
But to be able to write a framework using APT by yourself is to really understand it, so this article imitates butterknife Write your own to enhance your impression.

First let's look at a little bit of code

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
String key = getIntent().getStringExtra("key");
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // on click
    }
});

Get familiar, then get upset.
Every interface should be written like this to express tiredness.

Do you want to change your position?

@BindView(R.id.fab) FloatingActionButton fab;

@Intent("key") String key;

@OnClick({R.id.fab}) public void fabClick() { Toast.makeText(this, "Neacy", Toast.LENGTH_LONG).show(); }

That's right, so we can develop code efficiently and happily...

How does this work

Definition Notes

BindView, Intent, OnClick because of these things are so definitely annotations, roll up your sleeve to define these annotations immediately, which do not need to be written out to explain it casually:

@Retention(RetentionPolicy.CLASS)// Indicates that we are used to compile annotations
@Target(ElementType.FIELD)// Indicates that we are using attributes
public @interface BindView {
    int value();
}

AbstractProcessor to generate code

Must be the top class to implement AbstractProcessor Here's an initialized template

@AutoService(Processor.class)
public class NeacyProcesser extends AbstractProcessor {

    private Filer mFiler; //File-related auxiliary classes
    private Elements mElementUtils; //Element-related auxiliary classes
    private Messager mMessager; //Log-related auxiliary classes

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
        mMessager = processingEnvironment.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {// Related annotation classes to process
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        types.add(Intent.class.getCanonicalName());
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }
}

Once the "template" code above is written, the process in it is used to generate code, so here's the real difficulty in implementing APT. Let's also explain it with Intent:

for (Element element : roundEnvironment.getElementsAnnotatedWith(Intent.class)) {
   // ...some codes
}

With getElementsAnnotatedWith we can get Element objects from annotations, and at this point we'll explain some of the Element-related copy net examples (with examples at the end of the article).

package com.example;

public class Foo { // TypeElement If your comment is for working with classes

    private int a; // VariableElement If your comment is for processing attributes
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement If your comment is for processing

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

Take a look at the comments I added above and combine them with the code to understand why these elements are used.

     public JavaFile generateFinder() {
        // method inject(final T host, Object source, Provider provider)
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.PROVIDER, "provider");

        for (IntentField field : mIntents) {
            injectMethodBuilder.addStatement("host.$N = host.getIntent().getStringExtra($S)", field.getFieldName(), field.getKey());
        }
        // generate whole class
        TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
                .addMethod(injectMethodBuilder.build())
                .build();

        String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();

        JavaFile javaFile = JavaFile.builder(packageName, finderClass).build();
        return javaFile;
    }

This is using the magical Square company's open source JavaPoet to generate code, although string splicing is also possible.
Here's a JavaPoet article that you can read to understand what the above code is JavaPoet
Finally, in the process method

try {
         javaFile.writeTo(mFiler);
     } catch (IOException e) {
         return true;
  }

A java file will be generated soon, so here's a copy of the code generated in my project

public class MainActivity$$Finder implements Finder<MainActivity> {
  @Override
  public void inject(final MainActivity host, Object source, Provider provider) {
    host.fab = (FloatingActionButton)(provider.findView(source, 2131558523));
    View.OnClickListener listener;
    host.key = host.getIntent().getStringExtra("key");
    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.fabClick();
      }
    } ;
    provider.findView(source, 2131558523).setOnClickListener(listener);
  }
}

In fact, the main idea is to understand what these Elemet s mean, how to use them, and then splice the code using JavaPoet.

Finally, we'll add an intermediate layer of encapsulation similar to butterknife

    public static void inject(Object host, Object source, Provider provider) {
        String className = host.getClass().getName();
        try {
            Class<?> finderClass = Class.forName(className + FINDER_SUFFIX);
            Finder finder = mFinderArrayMap.get(className);
            if (finder == null) {
                finder = (Finder) finderClass.newInstance();
                mFinderArrayMap.put(className, finder);
            }
            finder.inject(host, source, provider);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

This way we can think of butterknife as having one line of code in the Activity, so all the tagged annotations are done for us.

NeacyFinder.inject(this);

Thank

http://brucezz.itscoder.com/use-apt-in-android
http://blog.csdn.net/crazy1235/article/details/51876192

Project Address

https://github.com/Neacy/NeacyFinder

Posted by glence on Fri, 21 Jun 2019 13:00:03 -0700