ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-01-30 17:01:29  阅读:327  来源: 互联网

标签:group val element ---- path fun KotlinPoet Android class


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
     }

标签:group,val,element,----,path,fun,KotlinPoet,Android,class
来源: https://blog.csdn.net/qq_33235287/article/details/122747001

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有