Android component design 3 -- advanced usage of kotlinpoet

Posted by bradymills on Wed, 02 Feb 2022 03:41:59 +0100

As a means of generating code by annotation processor, kotlinpool generates Kotlin code in an object-oriented way, which is more in line with the design idea of designers than the write code line by line of EventBus

1. Basic syntax of kotlinpoet

First write a piece of Kotlin code

class testPoet{

	companion object {
	    private const val TAG: String = "testPoet"
	}
        
    fun test(str:String){

        println(str)
    }
}

If you use kotlinpool to generate Kotlin code, according to the object-oriented design idea, first write the function test, then write the class testpool, add the function to the class, and then export the file

 override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {

   p1?.let {

       val elementsSet = it.getElementsAnnotatedWith(LayRouter::class.java)

       for (element:Element in elementsSet){
           
           //1 write the method test first
           val testMethod = FunSpec.builder("test")
               .addModifiers(KModifier.PUBLIC)
               .addParameter("str", String::class)
               .returns(UNIT)
               .addStatement("println(str)").build()

           //2 rewrite class
           val companion = TypeSpec.companionObjectBuilder()
               .addProperty(
                   PropertySpec.builder(
                       "TAG",
                       String::class,
                       KModifier.PRIVATE,
                       KModifier.CONST
                   ).initializer("%S", "testPoet").build()
               ).build()


           val classBuilder = TypeSpec.classBuilder("testPoet")
               .addModifiers(KModifier.PUBLIC)
               //Add method
               .addFunction(testMethod)
               .addType(companion)
               .build()

           //3 generate kotlin file
           val file = FileSpec.builder("", "TestPoet")
               .addType(classBuilder)
               .build()

           //export file
           file.writeTo(filer!!)

       }
   }

   return false
}

Here are several common class objects that need to be explained
1 FunSpec: used to generate functions, corresponding to MethodSpec in javapool
addModifiers: the access modifiers of the function, which are private, public, protect
addParameter: the parameter carried in the method in the format of "parameter name" and "parameter type". If there are multiple parameters, addParameter can be called multiple times
returns: the return value of the function
addStatement: function body, where * *% T * *% S can be used to realize placeholder,% T corresponds to $T of javapool, for example, some classes,% S is used for placeholder of string

2 TypeSpec: used to generate classes, consistent with javapool
addModifiers: add access modifiers
addFunction addType: adds a method or attribute to a class

3 PropertySpec: used to generate properties
initializer: if the attribute needs to be initialized, the format is "format initialized value" and the format is a placeholder

4 FileSpec: used to generate kotlin files and export them through Filer

If annotations are used in several places, several Kotlin files will be generated

Question 1: if annotations are used in multiple places in a module, will there be a problem if the Kotlin file is generated in the above way?
The answer must be yes, because the generated Kotlin file names are consistent, and such file names will conflict. Therefore, you can obtain the class name of annotation elements through element

val className = element.simpleName.toString()

2 generate simple routing addressing code through kotlinpool

For each Activity, if you want to obtain the class of the target Activity, you can save the corresponding class of each Activity as value according to the path through the map, and then take the class from the path to jump. The limitation of this method is that it needs to be registered manually, and in the face of a large number of activities, 1-to-1 registration is unrealistic, so it can be realized through APT

 class MainActivityARouter{

    companion object{

        fun findTargetClassName(path:String): KClass<MainActivity>? {

            return if(path == "app/MainActivity") MainActivity::class else null
        }
    }
}

This is the routing addressing code of MainActivity. Verify whether the path is consistent by entering path. If so, obtain the bytecode of MainActivity and jump

The code generated by kotlinpool here is no longer just a few words. For example, MainActivity and path need to be obtained dynamically, including the use of generics and the judgment of whether the return value can be empty

val annotation = element.getAnnotation(LayRouter::class.java)
//method
val funSpec = FunSpec.builder("findTargetClassName")
val companion = TypeSpec.companionObjectBuilder()
    .addFunction(
        funSpec
            .addModifiers(KModifier.PUBLIC)
            .returns(KClass::class.asTypeName().parameterizedBy(
                element.asType().asTypeName()
            ))
            .addParameter("path", String::class)
            .addStatement(
                "return if(path == %S) %T::class else null",
                annotation.path,
                element.asType().asTypeName())
            .build()

    ).build()
//Write class
val classBuilder = TypeSpec.classBuilder(className+"ARouter")
    .addType(companion).build()

val fileSpec = FileSpec.builder("", className+"Finding")
    .addType(classBuilder)
    .build()
fileSpec.writeTo(filer!!)

Only new points are mentioned here

1 for the data whose return value is generic type, kclass < mainactivity >, the parameters in generic type can be represented by parameterized by. If there are several parameters, you can choose to add several parameters

2 get the class of the annotation class. You can get the specific class type through Element

element.asType().asTypeName()

There is also an existing problem

**

Question: for Kotlin, can the return value be null? If implemented through KotlinPoet

**

3 handwriting ARouter framework

In the real componentization project, the code redundancy of route Jump realized in the above way is serious, and it is not necessary to generate a code for each annotation class, which should truly reflect the componentization system

Starting from the shell project of app, you can call up the pages of this group through routing, or call up the pages of other groups across modules, which can be realized through a global map

/**
 * element Each Activity is an element
 * model Class of each Activity
 */
class RouteBean(builder: Builder) {


    private var element: Element? = null
    private var model:KClass<*>? = null
    private var type: RouterType? = null
    private var path:String = ""
    private var group:String = "null"


    init {
        this.element = builder.element
        this.group = builder.group
        this.path = builder.path
    }

    companion object{

        fun create(model:KClass<*>,type: RouterType,path:String,group:String) : RouteBean{

            val bean = Builder
                .addGroup(group)
                .addPath(path)
                .build()
            bean.setType(type)
            bean.setModel(model)
            return bean
        }
    }
    /**
     * Builder pattern 
     */
    object Builder{

        var element: Element? = null
        var path:String = ""
        var group:String = ""

        fun addElement(element: Element):Builder{

            this.element = element
            return this
        }

        fun addGroup(group:String):Builder{

            this.group = group
            return this
        }

        fun addPath(path:String):Builder{

            this.path = path
            return this
        }

        fun build():RouteBean{
            return RouteBean(this)
        }

    }

    fun setType(type: RouterType){
        this.type = type
    }

    fun setModel(model:KClass<*>){
        this.model = model
    }

    fun getType():RouterType{
        return type!!
    }

    fun getModel():KClass<*>{
        return model!!
    }

    fun getGroup():String{
        return group
    }

    fun getPath():String{
        return path
    }

    fun getElement():Element{
        return element!!
    }
}


enum class RouterType{
    ACTIVITY,FRAGMENT
}

First, from the dimension of the group, there is a corresponding path under each group. The key is app, video, mine, etc. you get the path corresponding to the group, which is also a map; Key is app/MainActivity... value is the information of each annotation Class, including the corresponding Class

ARouter$$path$$app
ARouter$$path$$video
ARouter$$path$$mine

ARouter$$group$$app
ARouter$$group$$video
ARouter$$group$$mine

First write a basic template, and then use KotlinPoet to generate code

    //<"app","List<RouteBean>">
    //<"video","List<RouteBean>">
    //<"mine","List<RouteBean>">
    private val pathMap = mutableMapOf<String,MutableList<RouteBean>>()
    //Encapsulate the group's map
    private val groupMap = mutableMapOf<String,String>()

3.1 processing before generating routing code

 override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {

        p1?.let {

            val elementsSet = it.getElementsAnnotatedWith(LayRouter::class.java)

            for (element: Element in elementsSet) {

                val className = element.simpleName.toString()

                val annotation = element.getAnnotation(LayRouter::class.java)
                //Create RootBean
                val routeBean = RouteBean.Builder
                    .addElement(element)
                    .addGroup(annotation.group)
                    .addPath(annotation.path).build()

                //Judge whether it is the annotation on Activity or Fragment
                val activityMirror = elementTool!!.getTypeElement(ACTIVITY_PACKAGE).asType()

                if(typeTool!!.isSubtype(element.asType(),activityMirror)){

                    routeBean.setType(RouterType.ACTIVITY)
                }else{
                    //Throw exception
                    throw Exception("@LayRouter Annotations can only be used in Activity perhaps Fragment Use on")
                }

                //Group by group
                var pathChild:MutableList<RouteBean>? = pathMap[routeBean.getGroup()]

                if(pathChild.isNullOrEmpty()){

                    pathChild = mutableListOf()
                    //If it is empty
                    pathChild.add(routeBean)
                    //Add to map
                    pathMap[routeBean.getGroup()] = pathChild
                }else{
                    //If not empty
                    pathCild.add(routeBean)
                }
            }

            //Loop lookup complete
            message!!.printMessage(Diagnostic.Kind.NOTE,"see map $pathMap")

            createPathFile()
            createGroupFile()
        }

        return true
    }

Before generating code, you need to count the number of routing lines in each group before you can add all routing lines to the map in the getARoutePath function. Therefore, all annotation classes of each module need to be added to a pathMap, which is mainly prepared for adding code statements circularly

{app=[com.study.compiler_api.RouteBean@3a5c7c9d, com.study.compiler_api.RouteBean@3891f826]}

3.2 ARouter Path code generation

 class ARouter$$path$$app : IARoutPath{
        
   override fun getARoutePath(): MutableMap<String, RouteBean> {
       
       val pathMap = mutableMapOf<String,RouteBean>()
       
       pathMap.put("app/MainActivity",RouteBean.create(model,type,path,group))
       
       return pathMap
       
   }
}

Generate code using kotlinPoet

private fun createPathFile() {

        //From the pathMap, get the path

        val builder = FunSpec.builder("getARoutePath")
            .addModifiers(KModifier.OVERRIDE)
            //Return value
            .returns(
                MutableMap::class.asTypeName().parameterizedBy(
                String::class.asTypeName(),
                RouteBean::class.asTypeName()
            ))
            .addStatement("val %N = mutableMapOf<%T,%T>()",pathMap_Variable,String::class,RouteBean::class)
        //map addition needs to be put in the loop
        pathMap.forEach { (_, mutableList) ->

            //Judge the parameters transmitted from the module
            mutableList.forEach { routeBean ->

                builder.addStatement("%N.put(%S,%T.create(%T::class,%T.%L,%S,%S))",
                    pathMap_Variable,
                    routeBean.getPath(),
                    RouteBean::class,
                    routeBean.getElement().asType().asTypeName(),
                    RouterType::class,
                    routeBean.getType(),
                    routeBean.getPath(),
                    routeBean.getGroup()
                )
            }

        }

        builder.addStatement("return %N",pathMap_Variable)

        val className = "ARouter_path_$option"
        //create a file
        val fileBuilder = FileSpec.builder("", className)
            .addType(
                TypeSpec.classBuilder(className)
                    .addFunction(builder.build())
                    .addSuperinterface(ClassName("com.study.compiler_api","IARoutPath"))
                .build()).build()

        fileBuilder.writeTo(filer!!)

        //Add to groupMap
        if(!groupMap.containsKey(option)){

            groupMap[option] = className
        }

        message!!.printMessage(Diagnostic.Kind.NOTE,"groupMap $groupMap")

    }

Just say new knowledge points
1 placeholder% N, placeholder for variable, corresponding to $n in javapool
Placeholder% L, literal, such as enumerations and constants, can be used

2 addSuperinterface: implements a class or interface, and the parameter ClassName ("package name", "implemented class or interface")

Because all routes need to be registered in the map, the pathMap used in the element loop at the beginning is used, which encapsulates the list < rooutebean >, and all route information can be taken out

public class ARouter_path_app : IARoutPath {
  public override fun getARoutePath(): Map<String, RouteBean> {
    val pathMap = mutableMapOf<String,RouteBean>()
    pathMap.put("app/MainActivity",RouteBean.create(MainActivity::class,RouterType.ACTIVITY,"app/MainActivity","app"))
    pathMap.put("app/SecondActivity",RouteBean.create(SecondActivity::class,RouterType.ACTIVITY,"app/SecondActivity","app"))
    return pathMap
  }
}

After exporting all child routing addresses of a group, save the group name and the routing class name representing the group

3.3 ARouteGroup code generation

class ARouteGroupApp : IARoutGroup {
    override fun getARouteGroup(): Map<String, IARoutPath> {

        val groupMap = mutableMapOf<String, IARoutPath>()

        groupMap["app"] = ARouter_path_app()

        return groupMap
    }
}

Koltinpool generated code

 private fun createGroupFile() {

        val builder = FunSpec.builder("getARouteGroup")
            .addModifiers(KModifier.OVERRIDE)
            .returns(Map::class.asTypeName().parameterizedBy(
                String::class.asTypeName(),
                IARoutPath::class.asTypeName()
            ))
            .addStatement(
                "val %N = mutableMapOf<%T,%T>()",
                groupMap_Variable,
                String::class,
                IARoutPath::class
            )

        groupMap.forEach { (key, value) ->

            builder.addStatement(
                "groupMap[%S] = %T()",
                key,
                ClassName("",value)
            )
        }

        builder.addStatement("return %N",groupMap_Variable)

        val classBuilder = TypeSpec.classBuilder("ARouteGroup$option")
            .addFunction(builder.build())
            .addSuperinterface(ClassName("com.study.compiler_api","IARoutGroup"))
            .build()

        val file = FileSpec.builder("", "ARouteGroup$option")
            .addType(classBuilder).build()

        file.writeTo(filer!!)

    }

One thing here is to find the corresponding class in the package through the class name (string), that is, through ClassName. The first parameter is the package name, the package name of the class to be found, and value is the class name

ClassName("",value)

4 problem handling

If the apt code is generated in this way, there will be a problem. The attempt to reopen the file fails!!

Caused by: javax.annotation.processing.FilerException: Attempt to reopen a file for path 

The reason is that the process method will be executed twice, so it will be executed twice when writing apt code. After the Kotlin code is generated for the first time, an error will be reported when executing the process method again with the same file name

Online Q & A is to return process to true, which can be avoided if it doesn't work, but it doesn't work after trying

Solution:

process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

The process function has two parameters. set is not empty only once, and it is empty in other cases. Therefore, annotation processing can be carried out when it is not empty. If it is empty, it can directly return false, which is the key to the problem!!

//Processing annotations
 override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {

     if(p0!!.isEmpty()){

         return false
     }

Topics: Java Android kotlin apt