Conventional receiving parameter

class MainActivity : AppCompatActivity() {

    lateinit var user: String

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        user = intent.getStringExtra("user") ?: ""}}Copy the code

The normal way to receive parameters is in onCreate, using intent.getxxx, which is repetitive and simple, but important. For repeated things, we can use APT to solve.

APT generating file

Every time you use APT processing, you need to know what the files you want to generate look like.

MainActivityParameter. Java APT generated file

public class MainActivityParameter implements ParameterLoad {
  @Override
  public void loadParameter(Object target) {
    MainActivity t = (MainActivity)target;
    t.user = t.getIntent().getStringExtra("user"); }}Copy the code

MainActivityParameter Implements the loadParameter of the ParameterLoad interface, which is used to initialize the parameters required by the MainActivity.

ParameterLoad. Java APT specifies the interface to be implemented by the generated file

public interface ParameterLoad {

    /** * Target object. Attribute name = getIntent(). Attribute type (" annotation value or attribute name "); Complete the assignment * *@paramTarget The target object, such as MainActivity (some properties in) */
    void loadParameter(Object target);
}
Copy the code

Once you have the MainActivityParameter, you can use it in the MainActivity.

class MainActivity : AppCompatActivity() {
    lateinit var user: String

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        MainActivityParameter().loadParameter(this)}}Copy the code

The user attribute is initialized after onCreate calls MainActivityParameter().loadParameter(this).

Custom annotation

Now that you have the files APT is going to generate, you can start by defining property annotations.

@Target(AnnotationTarget.FIELD) // This annotation applies to attributes
@kotlin.annotation.Retention(AnnotationRetention.BINARY)
annotation class Parameter(
    // If the annotation value of name is left blank, the attribute name is the key
    val name: String = ""
)
Copy the code

Defining annotation handlers

Using an annotation handler requires adding an auto-service dependency, generating a file through Java’s object-oriented method requires Javapoet, and the final dependencies are as follows:

dependencies {
    implementation fileTree(dir: 'libs'.include: ['*.jar'])
    // Annotation handler
    kapt 'com. Google. Auto. Services: auto - service: 1.0 - rc6'
    api 'com. Google. Auto. Services: auto - service: 1.0 - rc6'

    // Help us generate Java code in the form of class calls
    implementation "Com. Squareup: javapoet: 1.9.0."
    // Introduce annotations and let the annotation handler - handle annotations
    implementation project(':annotation')}Copy the code

ParameterProcessor.java

// Register the annotation Processor Processor::class
@AutoService(Processor::class)
// Which annotation needs to be handled by the registry
@SupportedAnnotationTypes("com.loveqrc.annotation.Parameter")
// Specify the JDK build version
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// It is ok to define the parameters that the processor receives
// In the build.gradle file
// android {
// defaultConfig {
// kapt {
// arguments {
// arg("content", project.getName())
/ /}
/ /}
/ /}
/ /}
//
@SupportedOptions("content")

class ParameterProcessor : AbstractProcessor(a){
    // Operate the Element utility class (classes, functions, attributes are elements)
    lateinit var elementUtils: Elements

    //type(class information) utility class, which contains utility methods for manipulating TypeMirror
    lateinit var typeUtils: Types

    // Messager is used to report errors, warnings, and other information
    lateinit var messager: Messager

    // File generator class/resource, Filter is used to create new source files, class files, and auxiliary files
    lateinit var filer: Filer

    // Temporary map store, used to store the set of attributes annotated by @parameter, traversed during class file generation
    // key: class node, value: set of attributes annotated by @parameter
    private val tempParameterMap: HashMap<TypeElement, MutableList<Element>> = HashMap()


    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        elementUtils = processingEnv.elementUtils
        typeUtils = processingEnv.typeUtils
        messager = processingEnv.messager
        filer = processingEnv.filer

        val content: String? = processingEnv.options["content"] messager.printMessage(Diagnostic.Kind.NOTE, content ? :"content is null")}/** * handles the entry to the annotation, equivalent to the main function **@paramAnnotations Because the annotation handler can handle multiple annotations, an array * is returned@paramRoundEnv is the current or previous operating environment. You can use this object to find annotations. *@returnTrue indicates that the subsequent processor will not process (it has already processed) */
    override fun process( annotations: MutableSet
       
        , roundEnv: RoundEnvironment )
       : Boolean {
        if (annotations.isEmpty()) {
            return false
        }
        // Get the element annotated by @parameter
        val elements = roundEnv.getElementsAnnotatedWith(Parameter::class.java)

        if (elements.isEmpty()) {
            return false
        }
        // Get the value of the annotation and store it
        valueOfParameterMap(elements)
        // Generate the corresponding class file
        createParameterFile()

        return true
    }

    private fun createParameterFile(a) {
        if (tempParameterMap.isEmpty()) {
            return
        }
        // Use the Element utility class to get the corresponding type
        val activityType = elementUtils.getTypeElement(Constants.ACTIVITY) as TypeElement
        // Get the type of interface to implement
        val parameterType = elementUtils.getTypeElement(Constants.PARAMETER_LOAD) as TypeElement

        // Configure the (Object Target) parameter of the loadParameter method
        val parameterSpec: ParameterSpec =
            ParameterSpec.builder(TypeName.OBJECT, Constants.PARAMETER_NAME).build()

        // Each entry corresponds to an Activity
        tempParameterMap.entries.forEach {
            val typeElement = it.key
            // If it is not used in an Activity, then it is meaningless
            if(! typeUtils.isSubtype(typeElement.asType(), activityType.asType())) {throw RuntimeException("@Parameter only support for activity")}// Get the class name
            val className = ClassName.get(typeElement)

            // Method body content construction
            val factory = ParameterFactory.Builder(parameterSpec)
                .setMessager(messager)
                .setClassName(className)
                .build()

            // Add the first line of the method body content
            //MainActivity t = (MainActivity) target;
            factory.addFirstStatement()

            // Walk through all the attributes in the class
            for (fieldElement in it.value) {
                factory.buildStatement(fieldElement)
            }


            // Final generated class filename (class name Parameter)
            val finalClassName = typeElement.simpleName.toString() + Constants.PARAMETER_FILE_NAME
            messager.printMessage(
                Diagnostic.Kind.NOTE, "APT generate parameter class file:" +
                        className.packageName() + "." + finalClassName
            )

            // MainActivity$$Parameter
            JavaFile.builder(
                className.packageName(),  / / package name
                TypeSpec.classBuilder(finalClassName) / / the name of the class
                    .addSuperinterface(ClassName.get(parameterType)) // Implement ParameterLoad interface
                    .addModifiers(Modifier.PUBLIC) // public modifier
                    .addMethod(factory.build()) // Method construction (method parameters + method body)
                    .build()
            ) // The class is built
                .build() // JavaFile is built
                .writeTo(filer) // The file generator starts generating class files}}private fun valueOfParameterMap(elements: MutableSet<out Element>) {
        elements.forEach {
            messager.printMessage(Diagnostic.Kind.NOTE, it.simpleName)
            // Get the parent of the current Element
            val enclosingElement = it.enclosingElement as TypeElement
            // example:
            //class MainActivity : AppCompatActivity() {
            // @Parameter
            // var user: String? = null
            // }

            // it : user VariableElement
            // enclosingElement : MainActivity TypeElement
            messager.printMessage(Diagnostic.Kind.NOTE, enclosingElement.simpleName)

            if(tempParameterMap.containsKey(enclosingElement)) { tempParameterMap[enclosingElement]!! .add(it) }else {
                val fields = ArrayList<Element>()
                fields.add(it)
                tempParameterMap[enclosingElement] = fields
            }
        }
    }
}
Copy the code

ParameterProcessor registers the Processor, scans the attributes defined by @parameter, and stores them in a map based on the class they belong to. After the storage is complete, the file is generated through Javapoet. ParameterFactory is a encapsulated utility class,

Finally using

@Parameter
lateinit var user: String

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_test)
    TestActivityParameter().loadParameter(this)
    Log.e("TestActivity"."user:$user")}Copy the code

The source code

conclusion

APT is suitable for the scenario where you often write template code, such as to write APT function, first write down the pseudocode, and then generate files through the annotation processor.