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 }