introduce
APT(Annotation Processing Tool) is a Tool for Processing annotations. Specifically, it is a javAC Tool used to scan and process annotations at compile time. The annotation processor takes Java code (or compiled bytecode) as input and generates **.java files as output. In simple terms,.java** files are generated by annotations at compile time.
role
Use APT advantage is convenient, simple, can reduce a lot of repeated code.
Those of you who have used annotation frameworks such as ButterKnife, Dagger, EventBus, etc., can see how much less code can be created with these frameworks, just by writing some annotations. In fact, they just generated some code through annotations. Through learning APT, you will find that they are very strong ~~~
implementation
With all that said, try it
The goal is to implement a function with APT that binds a View by annotating a View variable (similar to @bindView in ButterKnife)
(Reference here)
Create a project Create an Android Module name app Create a Java Library Module name apt-Annotation Create a Java Library Module name apt-Processor dependency Apt-annotation Creates an Android library Module named apt-Library that relies on apt-annotation and auto-service
Structure is as follows
The function is mainly divided into three parts
apt-annotation
: custom annotation to store @bindViewapt-processor
: Annotation processor, according toapt-annotation
Annotations in, generated at compile timexxxActivity_ViewBinding.java
codeapt-library
: utility class, callxxxActivity_ViewBinding.java
In the method, implementationView
The binding.
Relations are as follows
The app? App is not a function code, just used to verify the function ~~~
1. Apt-annotation (custom annotations)
Create the annotation class BindView
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value(a);
}
Copy the code
@Retention(retentionPolicy.class) : indicates runtime annotation @target (elementtype.field) : indicates annotation scope is CLASS member (constructor, method, member variable)
RetentionPoicy.SOURCE, RetentionPoicy.CLASS, RetentionPoicy. Define the details of the modified object scope TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, etc
Here we define the runtime annotation BindView, where value() is used to get the ID of the corresponding View.
2, Apt-Processor (annotation processor)
(Key points)
Add dependencies to Module
dependencies {
implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2'
implementation project(':apt-annotation')}Copy the code
When Android Studio was upgraded to 3.0, Gradle was also upgraded to 3.0. So we’re replacing compile with implementation
Create BindViewProcessor
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Messager mMessager;
private Elements mElementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes(a) {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion(a) {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
// Generate Java files from annotations
return false; }}Copy the code
init
: Initializes. You can getProcessingEnviroment
.ProcessingEnviroment
Provides many useful utility classesElements
.Types
和Filer
getSupportedAnnotationTypes
: specifies which annotation handler is registered forBindView
getSupportedSourceVersion
: Specifies the Java version to use, usually returned hereSourceVersion.latestSupported()
process
: This is where you write code that scans, evaluates, and processes annotations, generating Java files (The code in process is detailed below)
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Messager mMessager;
private Elements mElementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
mProxyMap.clear();
// Get all the annotations
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new ClassCreatorProxy(mElementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxy.putElement(id, variableElement);
}
// Create a Java file by iterating through mProxyMap
for (String key : mProxyMap.keySet()) {
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
try {
mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true; }}Copy the code
Through roundEnvironment. GetElementsAnnotatedWith (BindView. Class) get all annotations elements, and then save the information elements to mProxyMap, Finally, create the corresponding Java file through mProxyMap, where mProxyMap is the Map collection of ClassCreatorProxy.
ClassCreatorProxy is a proxy class that creates Java code as follows:
public class ClassCreatorProxy {
private String mBindingClassName;
private String mPackageName;
private TypeElement mTypeElement;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
this.mTypeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
String packageName = packageElement.getQualifiedName().toString();
String className = mTypeElement.getSimpleName().toString();
this.mPackageName = packageName;
this.mBindingClassName = className + "_ViewBinding";
}
public void putElement(int id, VariableElement element) {
mVariableElementMap.put(id, element);
}
/** * Create Java code *@return* /
public String generateJavaCode(a) {
StringBuilder builder = new StringBuilder();
builder.append("package ").append(mPackageName).append("; \n\n");
builder.append("import com.example.gavin.apt_library.*; \n");
builder.append('\n');
builder.append("public class ").append(mBindingClassName);
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
/** * add Method *@param builder
*/
private void generateMethods(StringBuilder builder) {
builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append("host." + name).append("=");
builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + ")); \n");
}
builder.append(" }\n");
}
public String getProxyClassFullName(a)
{
return mPackageName + "." + mBindingClassName;
}
public TypeElement getTypeElement(a)
{
returnmTypeElement; }}Copy the code
Elements, TypeElement, variable type, ID, etc., and StringBuilder to spell out the Java code bit by bit. Each object represents a corresponding. Java file.
Surprise! Java code can also be written like this ~~ take a look at the generated code in advance.
public class MainActivity_ViewBinding {
public void bind(com.example.gavin.apttest.MainActivity host) {
host.mButton = (android.widget.Button) (((android.app.Activity) host).findViewById(2131165218));
host.mTextView = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165321)); }}Copy the code
Bugs Spelling Java code bit by bit through StringBuilder is tedious and easy to miswrite
A better solution would be to generate such Java code more easily with Javapoet. (More on that later)
1. Create a resources folder in the main directory of the Processors library. 2. Create META-INF/services directory in resources folder. 3, in the meta-inf/services directory folder to create javax.mail. Annotation. Processing. The Processor files; 4, the javax.mail. The annotation. Processing. Processor file is written to the full name of annotation Processor, including package path;) Is that too much trouble to write down? That’s why auto-service was introduced. AutoService annotation is automatically generated by @AutoService in auto-Service. The AutoService annotation processor was developed by Google. Used to generate the meta-inf/services/javax.mail annotation. Processing. The Processor file
3, apt-library utility class
We’ve finished the Processor part, and we’re almost done.
We created xxxactivity_viewbinding. Java in BindViewProcessor. Of course it’s reflection!!
Add dependencies to build.gradle in Module
dependencies {
implementation project(':apt-annotation')}Copy the code
Create the annotation tool class BindViewTools
public class BindViewTools {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch(InvocationTargetException e) { e.printStackTrace(); }}}Copy the code
The apt-Library part is relatively simple. Find the corresponding ViewBinding class by reflection, and then call the bind() method in it to complete the View binding.
So far, all the relevant code has been written, and it is finally ready to go
4, app
Dependencies in Module build.gradle (gradle >=2.2)
dependencies {
implementation project(':apt-annotation')
implementation project(':apt-library')
annotationProcessor project(':apt-processor')
}
Copy the code
Android Gradle plugin version 2.2, Android Gradle plugin provides a function called annotationProcessor to completely replace Android -apt
(if Gradle<2.2) In Project build. Gradle:
buildscript {
dependencies {
classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'}}Copy the code
In Module buile.gradle:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
apt project(':apt-processor')}Copy the code
In MainActivity, add the BindView annotation in front of the View and pass in the ID
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView mTextView;
@BindView(R.id.btn)
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewTools.bind(this);
mTextView.setText("bind TextView success");
mButton.setText("bind Button success"); }}Copy the code
I think we all know the results of this operation, but to prove that the BindView function is complete, I will post the image
Generated code
The above functionality has been doing one thing, generating Java code, so where is the generated code? In the app/build/generated/source/apt can be found in the generated Java files
public class MainActivity_ViewBinding {
public void bind(com.example.gavin.apttest.MainActivity host) {
host.mButton = (android.widget.Button) (((android.app.Activity) host).findViewById(2131165218));
host.mTextView = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165321)); }}Copy the code
Generate code from Javapoet
Above, in ClassCreatorProxy, StringBuilder is used to generate the corresponding Java code. This is cumbersome, but there is a more elegant approach, which is javapoet.
Add dependencies first
dependencies {
implementation 'com. Squareup: javapoet: 1.10.0'
}
Copy the code
And then in ClassCreatorProxy
public class ClassCreatorProxy {
// omit some code...
/** * Create Java code *@return* /
public TypeSpec generateJavaCode2(a) {
TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethods2())
.build();
return bindingClass;
}
/** * add Method */
private MethodSpec generateMethods2(a) {
ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(host, "host");
for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
methodBuilder.addCode("host." + name + "=" + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
}
return methodBuilder.build();
}
public String getPackageName(a) {
returnmPackageName; }}Copy the code
And then finally in the BindViewProcessor
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// omit some code...
// Generated by Javapoet
for (String key : mProxyMap.keySet()) {
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode2()).build();
try {
// Generate the file
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true;
}
Copy the code
It’s much cleaner and much simpler than using StringBuilder to spell out Java code. The resulting code is the same as before, so I won’t post it.
Javapoet usage details
The source code
GitHub
reference
Compile time annotations (APT) this is just the beginning of the compilation time annotation parsing framework You must know APT, annotationProcessor, Android-apt, Provided, custom annotations
Thank you for pointing out the above mistakes