KotlinPoet作为注解处理器生成代码的手段,相较于EventBus一行一行的write代码,KotlinPoet是采用面向对象的方式生成Kotlin代码,更符合设计人员的设计思路
KotlinPoet高级用法
1 KotlinPoet的基础语法
首先写一段Kotlin代码
class testPoet{
companion object {
private const val TAG: String = "testPoet"
}
fun test(str:String){
println(str)
}
}
如果使用KotlinPoet生成Kotlin代码,按照面向对象的设计思路,首先写函数test,然后写类testPoet,将函数添加到类中,然后导出文件
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 先写方法 test
val testMethod = FunSpec.builder("test")
.addModifiers(KModifier.PUBLIC)
.addParameter("str", String::class)
.returns(UNIT)
.addStatement("println(str)").build()
//2 再写类
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)
//添加方法
.addFunction(testMethod)
.addType(companion)
.build()
//3 生成kotlin文件
val file = FileSpec.builder("", "TestPoet")
.addType(classBuilder)
.build()
//导出文件
file.writeTo(filer!!)
}
}
return false
}
这里有常用的几个类对象需要解释一下
1 FunSpec:用于生成函数,对应JavaPoet中的MethodSpec
addModifiers:函数的访问修饰符,是private、public、protect …
addParameter:方法中携带的参数,格式为 ”参数名“ ”参数类型“,如果存在多个参数,addParameter可以调用多次
returns:函数的返回值
addStatement:函数体,其中使用**%T** %S可以实现占位,%T 对应JavaPoet的 $T,例如一些类,%S用于字符串的占位
2 TypeSpec:用于生成类,与JavaPoet一致
addModifiers:添加访问修饰符
addFunction addType:在类中添加方法或者属性
3 PropertySpec:用于生成属性
initializer:如果属性需要初始化,格式为 “format 初始化的值” format为占位符
4 FileSpec:用于生成kotlin文件,通过Filer导出
有几个地方用到了注解,那么就会生成几个Kotlin文件
问题1:如果在一个模块中,多处使用到了注解,按照上述的方式,生成Kotlin文件,会不会有问题?
答案肯定是有的,因为生成的Kotlin文件名都是一致的,这种文件名就会冲突,因此可以通过element来获取注解元素的类名
val className = element.simpleName.toString()
2 通过KotlinPoet生成简单的路由寻址代码
对于每个Activity,想要获取目标Activity的Class,可以通过map,根据path将每个Activity对应的class作为value保存,从path中取出class即可跳转,这种方式存在的局限在于需要手动注册,而且面对大量的Activity,1对1注册是不现实的,那么就可以通过APT的方式实现
class MainActivityARouter{
companion object{
fun findTargetClassName(path:String): KClass<MainActivity>? {
return if(path == "app/MainActivity") MainActivity::class else null
}
}
}
这是MainActivity的路由寻址代码,通过输入path验证路径是否一致,如果一致,那么就获取MainActivity的字节码,跳转
这里通过KotlinPoet生成代码,就不再是简单的输入几句话就可以的,像MainActivity,path等都是需要动态获取的,包括泛型的使用,返回值是否可为空的判断
val annotation = element.getAnnotation(LayRouter::class.java)
//方法
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()
//写类
val classBuilder = TypeSpec.classBuilder(className+"ARouter")
.addType(companion).build()
val fileSpec = FileSpec.builder("", className+"Finding")
.addType(classBuilder)
.build()
fileSpec.writeTo(filer!!)
这里只说新的点
1 对于返回值是泛型类型的数据KClass<MainActivity>
,其中泛型中的参数,可以通过parameterizedBy表示,有几个参数就选择添加进去几个参数
2 获取注解类的Class,可以通过Element来获取具体的class类型
element.asType().asTypeName()
这里还现存一个问题
**
问题:对于Kotlin中,返回值允许为空的 ? 如果通过KotlinPoet实现
**
3 手写ARouter框架
在真正的组件化工程中,以上的方式实现路由跳转代码冗余严重,而且没必要每个注解类都生成一份代码,要真正体现组件化的体系
从app的壳工程出发,可以通过路由的方式调起本group的页面,也可以跨模块调起其他group的页面,可以通过一个全局的map来实现
/**
* element 每个Activity都是一个元素
* model 每个Activity的Class
*/
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
}
}
/**
* 建造者模式
*/
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
}
先从组的维度,每个组下面都有对应的path,key就是app、video、mine等等,拿到了group对应的path,也是一个map;key就是app/MainActivity … value是每个注解类的信息,包括对应的Class
ARouter$$path$$app
ARouter$$path$$video
ARouter$$path$$mine
ARouter$$group$$app
ARouter$$group$$video
ARouter$$group$$mine
先写基础的一个样板,然后使用KotlinPoet生成代码
//<"app","List<RouteBean>">
//<"video","List<RouteBean>">
//<"mine","List<RouteBean>">
private val pathMap = mutableMapOf<String,MutableList<RouteBean>>()
//封装group的map
private val groupMap = mutableMapOf<String,String>()
3.1 生成路由代码前的处理
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)
//创建RootBean
val routeBean = RouteBean.Builder
.addElement(element)
.addGroup(annotation.group)
.addPath(annotation.path).build()
//判断是Activity上的注解还是Fragment上的
val activityMirror = elementTool!!.getTypeElement(ACTIVITY_PACKAGE).asType()
if(typeTool!!.isSubtype(element.asType(),activityMirror)){
routeBean.setType(RouterType.ACTIVITY)
}else{
//抛出异常
throw Exception("@LayRouter注解只能在Activity或者Fragment上使用")
}
//根据group分组
var pathChild:MutableList<RouteBean>? = pathMap[routeBean.getGroup()]
if(pathChild.isNullOrEmpty()){
pathChild = mutableListOf()
//如果是空的
pathChild.add(routeBean)
//添加到map中
pathMap[routeBean.getGroup()] = pathChild
}else{
//如果不为空
pathCild.add(routeBean)
}
}
//循环查找完成
message!!.printMessage(Diagnostic.Kind.NOTE,"查看map $pathMap")
createPathFile()
createGroupFile()
}
return true
}
在生成代码之前,先需要统计每个组内有多少路由线,才能在getARoutePath函数体内,将所有的路由线路添加到map中,因此对每个模块的全部注解类,需要添加到一个pathMap中,主要是为了循环添加代码语句准备的
{app=[com.study.compiler_api.RouteBean@3a5c7c9d, com.study.compiler_api.RouteBean@3891f826]}
3.2 ARouter Path代码生成
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
}
}
使用kotlinPoet生成代码
private fun createPathFile() {
//从pathMap中,获取path
val builder = FunSpec.builder("getARoutePath")
.addModifiers(KModifier.OVERRIDE)
//返回值
.returns(
MutableMap::class.asTypeName().parameterizedBy(
String::class.asTypeName(),
RouteBean::class.asTypeName()
))
.addStatement("val %N = mutableMapOf<%T,%T>()",pathMap_Variable,String::class,RouteBean::class)
//map添加需要放循环里
pathMap.forEach { (_, mutableList) ->
//判断模块传过来的参数
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"
//创建文件
val fileBuilder = FileSpec.builder("", className)
.addType(
TypeSpec.classBuilder(className)
.addFunction(builder.build())
.addSuperinterface(ClassName("com.study.compiler_api","IARoutPath"))
.build()).build()
fileBuilder.writeTo(filer!!)
//往groupMap里添加
if(!groupMap.containsKey(option)){
groupMap[option] = className
}
message!!.printMessage(Diagnostic.Kind.NOTE,"groupMap $groupMap")
}
只说新的知识点
1 占位符 %N,变量的占位符,对应JavaPoet中的$N
占位符 %L,字面量,像一些枚举、常量等可以使用
2 addSuperinterface :实现某个类或者接口,参数ClassName(“包名”,“实现的类或者接口”)
因为需要将所有的路由注册到map中,因此就用到了一开始在element循环中使用到的pathMap,其中封装了List<RoouteBean>
,可以把所有的路由信息取出来
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
}
}
在导出某个组的全部孩子路由地址之后,将group名称和代表该组的路由类名称保存起来
3.3 ARouteGroup代码生成
class ARouteGroupApp : IARoutGroup {
override fun getARouteGroup(): Map<String, IARoutPath> {
val groupMap = mutableMapOf<String, IARoutPath>()
groupMap["app"] = ARouter_path_app()
return groupMap
}
}
koltinPoet生成代码
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!!)
}
这里有一点,就是通过类名(字符串),找到在包中对应的类,就是通过ClassName,第一个参数是包名,要查找的类所在的包名,value就是类名称
ClassName("",value)
4 问题处理
如果按照这种方式来生成apt代码,会有一个问题,尝试重新打开文件失败!!
Caused by: javax.annotation.processing.FilerException: Attempt to reopen a file for path
原因就是,process方法会被执行2次,那么在写apt代码的时候,就会被执行2次,第一次生成的Kotlin代码后,相同的文件名,再次执行process方法的时候会报错
网上有问答是把process返回为true,不工作了就能够避免,但是尝试之后并没有效果
解决方案:
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
process函数两个参数,set只有1次情况下不为空,其他情况下为空,因此在不为空的情况下可以进行注解处理,如果为空,那么就直接返回false即可,这才是问题的关键!!
//处理注解
override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
if(p0!!.isEmpty()){
return false
}