What is APT?

APT Annotation Processing Tool APT Annotation Processing Tool APT is a Tool used to process annotations. The Java files are scanned and the annotation generation files processed before the JVM compiles them to class files. Since this is pre-compile processing, it is often used to generate source files, whether Java or Kotlin.

What can APT do?

It does everything code can do before compiling the flow, mainly generating files, source files, or configuration files.

Pros and cons of APT?

  • Advantages: Obviously, the ability to do some simple processing before getting into the compile process is a boon to anyone who needs to process files before they are compiled
  • Cons: Its strength is also its weakness, which is that it can’t handle the post-compile process

How to use APT?

The statement notes

Create a New Java library, call it apt-Annotation, and create an annotation class annotation Class BindView. Here we’ll name the class after ButterKnife. The main content of the annotation class is as follows

package com.kaithmy.apt_annotation

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)

annotation class BindView(
    val value: Int
)
Copy the code

Where the variable value is used to store the component ID that needs to be bound to the component variable, such as @bindView (R.idvsend). Of course, AGP (Android Gradle Plugin) no longer ensures that the generated resource ID is immutable after 5.0. Let’s do it this way. If you need functionality similar to ButterKnife, the Kotlin Android Extension is recommended, although officials have recently decided to scrap it and instead promote ViewBinding.

Declare annotation handler

Create a New Java library named apt-Processor and a new annotation processing class named BindViewProcessor that inherits AbstractProcessor and rewrite its two methods, init and process. As the name implies, init is our initialization method, and process is our main method for handling annotations, and the logic for handling annotations is mostly in the Process method. We also have to rewrite getSupportedAnnotationTypes method, his return value represents the we can handle the annotation types, of course, we can also rewrite getSupportedSourceVersion method to support the JDK version of statement to Here we declare support for JDK1.8.

   // SupportedAnnotationTypes(" com.kaithmy.apt_annotation.bindView ") can be used instead
    override fun getSupportedAnnotationTypes(a): MutableSet<String> {
        return mutableSetOf(BindView::class.java.canonicalName)
    }

    // The supported JDK version can be @supportedSourceversion (SourceVersion.RELEASE_8) instead
    override fun getSupportedSourceVersion(a): SourceVersion {
        return SourceVersion.RELEASE_8
    }
Copy the code

If you don’t want to overwrite these two methods every time, we can use annotations instead, adding annotations to our BindViewProcessor, as in

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.kaithmy.apt_annotation.BindView")
class BindViewProcessor : AbstractProcessor() {... }Copy the code

Learn about the init method and its four main utility classes

The init method passes us a parameter of type ProcessingEnvironment, processingEnv, which, as the name suggests, is the annotation ProcessingEnvironment.

Calling its getFiler method yields a Filer type filerUtils, a file management utility class that operates on file-related classes.

Calling its getTypeUtils method yields a typeUtils of Types, which is a type handling utility class that deals with type-related classes.

Calling its getElementUtils method yields an elementUtils of type Elements, the Element handling utility class, which is primarily used to process Elements structured by the class

Calling its getMessager method yields a messagerUtils of type Messager, the logging utility class, which is used to output logs during processing

Declare API entry

We will create an interface IBindHelper and provide an Inject method as follows:

package com.kaithmy.apt_api

interface IBindHelper {
    fun inject(target: Any?).
}
Copy the code

Then create a class of type Object, KBind, and provide inject method (it can be named by any other name, not to be confused with the inject method in the interface) as follows:

 fun inject(target: Any?).{ target ? :return
        val qualifiedName = target::class.qualifiedName
        val helperClassName = "$qualifiedName\$AutoBind"

        val helperInstance =
            Class.forName(helperClassName).getConstructor().newInstance() as? IBindHelper ? :return
        helperInstance.inject(target)
    }
Copy the code

HelperClassName (qualifiedName) is a new Class that needs to be generated for qualifiedName. It is a new Class that needs to be generated for helperClassName. Inject method findViewById operation and assign value, and then realize injection.

Learn about the Process method, find annotations, and save annotation information

See the following code for the specific process:

    /** * Note 1: * process() is executed twice because the first input to the class did not output the file, and the second input is empty. ** Note 2: * Returns the value true to indicate that the annotations have been declared to be processed and do not require subsequent processors to process them; False indicates that annotations are undeclared and may require subsequent processors to process them */
    override fun process(
        annotations: MutableSet<out TypeElement>? , roundEnv:RoundEnvironment?).: Boolean {
        println("process start ========")
        if (annotations.isNullOrEmpty() || roundEnv == null) return false
        // All elements annotated by BindView
        val bindViewElements = roundEnv.getElementsAnnotatedWith(BindView::class.java)
        / / are classified by different host, such as AActvity BActivity, AFragment, etc
        categories(bindViewElements)
        bindMap.forEach {
            // Generate the corresponding code
            generateCode(it)
        }
        println("process end ========")
        return true
    }

    /** * Collect annotated information */
    private fun categories(bindViewElements: MutableSet<out Element>) {
        // Filter out the element annotated by BindView
        bindViewElements.forEach {
            // Get the corresponding upper element
            val enclosingElement = it.enclosingElement
            // Put views into bindMap
            var views = bindMap[enclosingElement]
            if (views == null) {
                views = hashSetOf()
                bindMap[enclosingElement] = views
            }

            // put the variable name and id into views
            val annotation = it.getAnnotation(BindView::class.java)
            val id = annotation.value
            views.add(
                ViewInfo(it.simpleName.toString(), id, it.asType())
            )
        }
    }
Copy the code

Generate object file

Through the above steps, we will have the corresponding annotation information preserved, then we need to do is to generate the code, the generated code at present from three ways, one is directly through the StringBuilder string concatenation to generate the target code, the other one is by auxiliary tool to generate the target code, due to the use of language is different, So it is subdivided into two, JavaPoet and KotlinPoet

JavaPoet generated

        val SUFFIX = "\$AutoBind"
        val elementName = entry.key.simpleName.toString()
        val packageName = elementUtils.getPackageOf(entry.key).qualifiedName.toString()

        val methodSpecBuilder = MethodSpec.methodBuilder("inject")
            .addAnnotation(Override::class.java)
            .addModifiers(Modifier.PUBLIC)
            .addParameter(Any::class.java, "target")
            .addCode("$elementName ins = ($elementName)target; \n")

        entry.value.forEach {
            methodSpecBuilder
                .addCode(
                    "ins.set${ it.viewName.replaceFirstChar { first -> first.toUpperCase() }
                    }(ins.findViewById(${it.resId})); \n")}val typeSpec = TypeSpec.classBuilder("$elementName$SUFFIX")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(IBindHelper::class.java)
            .addMethod(methodSpecBuilder.build())
            .build()

        JavaFile.builder(packageName, typeSpec).build().writeTo(filerUtils)
Copy the code

KotlinPoet generated

        val funBuilder = FunSpec.builder("inject")
            .addModifiers(KModifier.OVERRIDE)
            .addParameter("target", Any::class.asTypeName().copy(nullable = true))
            .addCode("val ins = target as? $elementName? :return \n")

        entry.value.forEach {
            funBuilder.addCode(
                "ins.${it.viewName} = ins.findViewById<${it.type}> (${it.resId}) \n")}val fileSpec = FileSpec.builder(packageName, "$elementName$SUFFIX")
            .addType(
                TypeSpec.classBuilder("$elementName$SUFFIX")
                    .addSuperinterface(IBindHelper::class)
                    .addFunction(funBuilder.build())
                    .build()
            ).build()
        fileSpec.writeTo(filerUtils)
Copy the code

The PROCESSOR is enabled through the SPI mechanism

SPI (Service Provicer Interface) is a built-in Service discovery mechanism in the JDK. In the SRC/main module apt – processor, we create a meta-inf folder, in which a new subfolder services, and in the services to create a new file javax.mail. The annotation. Processing. The processor, And in the file write our full name of the class of BindViewProcessor com. Kaithmy. Apt_processor. BindViewProcessor, at this point, our BindViewProcessor has to take effect.

Avoid the tedious SPI process with AutoServcie

Always feel the above operation is a bit tedious, so is there any way to avoid this tedious step? We can avoid this by introducing AutoServcie. AutoServices dependencies in apt-Processor

    api "Com. Google. Auto. Services: auto - service: 1.0 rc7." "
    kapt "Com. Google. Auto. Services: auto - service: 1.0 rc7." "
Copy the code

Then add the class annotation @AutoService(Processor:: Class) to our BindViewProcessor

How to debug?

What do we do when we need to debug the processor?

Configuration gradle. The properties

Add the following to gradle.properties under project:

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
-Dorg.gradle.debug=true
Copy the code

Configure Remote Debug

Create a new Remote in Run/Debug Configurations and name it whatever you want, in this case ProcessorDebug, click OK to save and then select the ProcessorDebug you just created in the IDE and hit Run orDebug to launch.

Breakpoint and build trigger

Clean Project to clean the existing build folder to prevent the files from being affected, then create a breakpoint where you want it and run build/ Assemble. /gradlew app:build /gradlew app:build

How do I access a custom processor and use it?

In your app moudle, introduce apt-annotation and apt-Processor. Note that Java uses Annotation Processor and Kotlin uses Kapt.

Then add annotations where you need them, as shown in the following example:

class MainActivity : AppCompatActivity() {
    @BindView(R.id.tvOpen)
    var tvOpen: TextView? = null

    @BindView(R.id.tvClose)
    var tvClose: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        KBind.bind(this) tvOpen? .setOnClickListener { Toast.makeText(this."Open clicked", Toast.LENGTH_SHORT).show()
        }
    }
}
Copy the code

Tips

How to monitor Gradle Task time and count total compile time?

Gradle provides TaskExecutionListener and BuildListener callback interfaces for Task execution. Using these two callback interfaces, we can easily count the time spent by each Task and total time spent by each Task. Task details will not be discussed later, but trasform will be discussed later. Directly add the following code to build. Gradle under project


import java.util.concurrent.TimeUnit

class TimingsListener implements TaskExecutionListener, BuildListener {
    private long startTime
    private timings = []

    @Override
    void beforeExecute(Task task) {
        startTime = System.nanoTime()
    }

    @Override
    void afterExecute(Task task, TaskState taskState) {
        def ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
        timings.add([ms, task.path])
        task.project.logger.log(LogLevel.WARN, "${task.path} took ${ms}ms")
    }

    @Override
    void buildStarted(Gradle gradle) {

    }

    @Override
    void settingsEvaluated(Settings settings) {

    }

    @Override
    void projectsLoaded(Gradle gradle) {

    }

    @Override
    void projectsEvaluated(Gradle gradle) {

    }

    @Override
    void buildFinished(BuildResult buildResult) {
        println("Task timings")
        for (timing in timings) {
            printf "%sms %s\n", timing
        }
    }
}

gradle.addListener(new TimingsListener())
Copy the code