Android组件化设计3 ---- KotlinPoet的高级用法

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导出
Android组件化设计3 ---- KotlinPoet的高级用法
有几个地方用到了注解,那么就会生成几个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框架

在真正的组件化工程中,以上的方式实现路由跳转代码冗余严重,而且没必要每个注解类都生成一份代码,要真正体现组件化的体系

Android组件化设计3 ---- KotlinPoet的高级用法

从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
     }

上一篇:SQL注入常用爆库语句


下一篇:object关键字:对象声明 对象表达式 伴生对象