kotlin MVVM+retrofit2+协程 Repository层apt优化方案

  • Repository层是整个架构数据来源的地方,包括网络和数据库等
    项目模块化呢,又会让每个coder要么维护同一个公共模块定义接口的类,外加Repository类,要么维护多个自己模块,多个Repository类。同一类操作带来代码管理冲突,只有每个人维护各自的接口类最合适。所以,下面就用apt对多个接口的方案实行优化
  1. 创建apt-annotation和apt-repository的kotlin Library
    apt-annotation定义注解,apt-repository实现AbstractProcessor然后自动生成kotlin代码


    自定义注解模块

    apt-annotation中自定义注解

/**
 * 网络请求方法的注解
 */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
public annotation class Repository(val value : RetrofitLinkType = RetrofitLinkType.RETROFIT_DEFAULT)

RetrofitLinkType只是为了给方法添加一个标识,表明该方法是做什么的,方便日志拦截打印数据出来,文件就不需要打印那么多body内容。打印也是乱码

/**
 * 给RetrofitManager添加注解
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class RetrofitManager()

我们一般会封装一个RetrofitManager类来管理retrofit和添加一些请求头、拦截器等,就按照我目前项目来写,RetrofitManager类暴露两个方法
~~这里定死该方法,以便apt生成Repository类获取RetrofitManager,当然也可以自己再定义注解来获取。为了方便就写死吧

//获取RetrofitManager对象
public static RetrofitManager getInstance(RetrofitLinkType type){
        return  new RetrofitManager(type);
    }
//创建网络api接口文件
    public <T> T create(Class<T> service){
        return retrofit.create(service);
    }

enum class RetrofitLinkType {
/**
* 默认请求
*/
RETROFIT_DEFAULT,

/**
 * 文件请求
 */
RETROFIT_FILE,
RETROFIT_DEFAULT2,
RETROFIT_DEFAULT3

}
2.apt-repository实现
创建RepositoryProcessor类继承AbstractProcessor

  • 在main文件夹下创建resources文件夹,再创建META-INF文件夹,再创建service文件夹。添加名为javax.annotation.processing.Processor的文件
    内容写上刚才创建的xxx(包名).RepositoryProcessor
    当然你也可以使用谷歌的AutoService


  • apt-repository模块gradle文件配置
plugins {
    id 'java-library'
    id 'org.jetbrains.kotlin.jvm'
    id 'kotlin'
    id 'kotlin-kapt'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {

    implementation project(path: ':apt-annotation')
//    annotationProcessor "com.google.auto.service:auto-service:1.0.1"
//    implementation "com.google.auto.service:auto-service:1.0.1"
    implementation 'com.squareup:kotlinpoet:1.12.0'
}

apt-repository模块中build.gradle文件JavaVersion.VERSION_1_7需要换成JavaVersion.VERSION_1_8

  • RepositoryProcessor实现
    进入正题:该类是对整个项目注解进行扫描处理的类,涉及到Filer(生成文件所需)Element(每个被注解的元素)
    首先定义一个map集合。key保存每个模块的api接口文件类名,map保存一个创建Repository类的对象ClassCreatorProxy,该对象持有全部添加@Repository注解的api接口方法。
    ClassCreatorProxy实现了生成Repository代码的规则,并最终生成一个(api接口类名+_repository)的单例模式的对象类,processor代码如下
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class RepositoryProcessor : AbstractProcessor() {
    //日志控制
    private var messager: Messager? = null
    //生成文件
    private var filer: Filer? = null
    private var mElementUtils: Elements? = null
    private val mProxyMap: HashMap<String, ClassCreatorProxy> = HashMap()
 
    override fun init(processingEnv: ProcessingEnvironment?) {
        super.init(processingEnv)
        filer = processingEnv?.filer
        messager = processingEnv!!.messager
        mElementUtils = processingEnv.elementUtils
        messager!!.printMessage(Diagnostic.Kind.NOTE, "APT-------------------初始化")
    }
    /**
     * 定义支持的注解类型,只需要扫描我们自定义的就够了
     */
    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        val set= mutableSetOf<String>()
        set.add(Repository::class.java.canonicalName)
        set.add(RetrofitManager::class.java.canonicalName)
        return set
    }

    override fun process(p0: MutableSet<out TypeElement>, roundEnvironment: RoundEnvironment?): Boolean {
        if (p0.isEmpty()) return  false
        //获取项目添加@RetrofitManager唯一的retrofitManager类,需要该类暴露getInstance和service方法,见上面的描述
        var retrofitManager:String?=null
        val annotatedTypes1: Set<Element?> =
            roundEnvironment?.getElementsAnnotatedWith(RetrofitManager::class.java)!! //类注解
        for (element in annotatedTypes1) {
            val typeElement = element as TypeElement
            retrofitManager = typeElement.qualifiedName.toString()//全路径
        }
        if(retrofitManager==null)return false
        //获取api接口类上添加@Repository的方法注解
        val annotatedTypes: Set<Element?> =
            roundEnvironment.getElementsAnnotatedWith(Repository::class.java)!! //类注解
        //遍历元素
        for (element in annotatedTypes) {
            val methodElement = element as ExecutableElement
            //获取全类名
            val classElement = methodElement.enclosingElement as TypeElement //被一个范围包裹的外层(包裹方法的就只有类了)
            val fullName = classElement.qualifiedName.toString()
            messager!!.printMessage(Diagnostic.Kind.NOTE, fullName)
            //看内存缓存中是否有对应的ClassCreatorProxy类
            var proxy: ClassCreatorProxy? = mProxyMap[fullName]
            if (proxy == null) {
                proxy = mElementUtils?.let { ClassCreatorProxy(it, classElement,retrofitManager!!) }
                if (proxy != null) {
                    mProxyMap[fullName] = proxy
                }
            }
            //向每个ClassCreatorProxy对象添加api中方法。
            proxy?.addMethodName(methodElement)
        }
        //通过遍历mProxyMap,创建kotlin文件
        for (key in mProxyMap.keys) {
            val proxyInfo = mProxyMap[key]

            val classFile: FileSpec =
                FileSpec.builder(proxyInfo!!.getPackageName(), proxyInfo.getClassName())
                    .addType(proxyInfo.generateJavaCode2()!!)
                    .build()
            try {
                //生成kotlin文件
                filer?.let { classFile.writeTo(it) }
            } catch (e: IOException) {
                messager?.printMessage(
                    Diagnostic.Kind.NOTE,
                    " --> create " + proxyInfo.getProxyClassFullName() + "error"
                )
            }
        }


        return true
    }
}
  • ClassCreatorProxy实现
    java是使用的javapoet,而kotlin是使用的kotlinpoet
    gradle添加 implementation 'com.squareup:kotlinpoet:1.12.0'
    当然也可以自己慢慢用字符串拼接。这个过程可能会出人命。
    接下来的思路是生成一个单例类型repository类
    第一步:TypeSpec.companionObjectBuilder()生成伴生对象


    生成属性

    生成方法体

    第二步:FunSpec.builder生成各个方法

kotlinpot在对Any类型或者String类型的处理上会处理成java.lang.Object和java.lang.String。这样就会把最终生成的代码搞成java的类型。导致方法参数类型对应不上。比如


kotlin代码是kotlin.String,apt生成的代码是java.lang.String

同理,Any类型的会生成Object类型。这不是我要的那种结果。

使用ExecutableElement元素可以拿到该方法名字以及参数名,参数类型,还有返回值,返回值类型
将其中的java.lang.Object替换成Any,java.lang.String替换成String,由于没发现里面有获取类型和修改的方法,只能转成字符串替换,再用String::class.asTypeName()得到TypeName传递给ParameterSpec.builder以便生成具体的参数和名字


处理参数类型

ExecutableElement在获取到api接口方法的时候。由于协程需要使用了suspend关键字,会把返回值实体包裹在kotlin.coroutines.Continuation中。当成参数传递。

kotlin.coroutines.Continuation<in java.lang.String>

就导致了ExecutableElement在getReturnType的时候获取到的是一个object对象。我们反而不能使用getReturnType来生成返回值了,需要在参数中最后一个参数去找到参数类型。把其中的泛型包裹的对象取出来传递给FunSpec.builder.returns()。

由于kotlinPoet不是很熟练。所以不清楚具体方法。只能用老办法字符串替代。
最后用TypeVariableName接收字符串传递给FunSpec.builder.returns()

ClassCreatorProxy类完整代码如下

class ClassCreatorProxy(elementUtils: Elements, classElement: TypeElement?, private val managerPath:String) {
    private var mClassName: String? = null//
    private var mPackageName: String? = null
    private var mTypeElement: TypeElement? = null
    private var apiService:ClassName//retrofit 接口类
    private var thisClassName:ClassName//需要生成的单例类
    /**
     * 方法集合
     */
    val executableElements: MutableList<ExecutableElement> = mutableListOf()

    init {
        mTypeElement = classElement
        val packageElement = elementUtils.getPackageOf(mTypeElement)
        val packageName = packageElement.qualifiedName.toString()
        val className = mTypeElement!!.simpleName.toString() //只获取类名
        mPackageName = packageName
        mClassName = className + "_repository"
        apiService = ClassName(getPackageName(), mTypeElement!!.simpleName.toString())
        thisClassName=ClassName(getPackageName(),getClassName())
    }

    /**
     * 添加方法到executableElements缓存
     * @param element
     */
    fun addMethodName(element: ExecutableElement) {
        executableElements.add(element)
    }
/////////----------------------------------
    /**
     * 生成伴生对象
     */
    private fun generateCompanion():TypeSpec{
        return TypeSpec.companionObjectBuilder()
            .addProperty(generateProperty())
            .addProperty(generateProperty2())
            .addFunction(generateFunction("getInstance"))
            .build()
    }

    /**
     * 生成伴生对象中属性mApiService
     */
    private fun generateProperty():PropertySpec{
        return PropertySpec.builder("mApiService",  apiService.copy(nullable = true))//可空参数
            // 初始化值
            .initializer("null")
            // 修饰符
            .addModifiers(KModifier.PRIVATE)
            .mutable()//var
            // 注释
            .build()
    }
    /**
     * 生成伴生对象中属性instance
     */
    private fun generateProperty2():PropertySpec{
        return PropertySpec.builder("instance",thisClassName)
            .initializer("%T()",thisClassName)//默认值是new class对象
            // 修饰符
            .addModifiers(KModifier.PRIVATE)
            .build()
    }
    /**
     * 生成getInstance方法体
     */
    private fun generateFunction(funName:String):FunSpec{
        val params=ParameterSpec.builder("type", RetrofitLinkType::class)
            .defaultValue("${RetrofitLinkType::class.java.`package`.name}.RetrofitLinkType.%L", RetrofitLinkType.RETROFIT_DEFAULT)
            .build()
        return FunSpec.builder(funName)
            .addParameter(params)
            .returns(thisClassName)
            .addStatement("mApiService= %L.getInstance(type).create(%L)",managerPath,apiService.simpleName+"::class.java")
            .addStatement("return instance")
            .build()
    }
    /**
     * 最后调用创建Java代码   javapoet
     * @return
     */
    fun generateJavaCode2(): TypeSpec? {
        val cb=FunSpec.constructorBuilder()
            .addModifiers(KModifier.PRIVATE)
            .build()
        return mClassName?.let {
            TypeSpec //class名称设置
                .classBuilder(it) //类为public
                .addFunction(cb)//私有构造函数
                .addType(generateCompanion())
                .addModifiers(KModifier.PUBLIC)
                .addFunctions(generateMethods2())
                .build()
        }
    }

    /**
     * 生成调用的方法
     */
    @OptIn(DelicateKotlinPoetApi::class)
    private fun generateMethods2(): Iterable<FunSpec> {
        val hashMap= mutableListOf<FunSpec>()
        executableElements.forEach {
            //获取到方法里面的参数
            val params= mutableListOf<ParameterSpec>()
            val parameters = it.parameters
            val returnParams=StringBuilder()

            var returnName =TypeVariableName("Any")
            parameters.forEachIndexed { index, param->
                var asType = param.asType().asTypeName()
                //kotlin协程是将返回值包裹成kotlin.coroutines.Continuation<T>当做参数传递的
                if (asType.toString().contains("kotlin.coroutines.Continuation")){
                    var asTypeString=asType.toString()
                    if(asTypeString.contains("java.lang.Object")){
                        asTypeString = asType.toString().replace("java.lang.Object", "Any")
                    }else if(asTypeString.contains("java.lang.String")){
                        asTypeString = asType.toString().replace("java.lang.String", "String")
                    }
                    asTypeString = asTypeString.removeSurrounding("kotlin.coroutines.Continuation<in ", ">")
                    asTypeString = asTypeString.removeSurrounding("kotlin.coroutines.Continuation<", ">")
                    returnName = TypeVariableName(asTypeString)
                }else{
                    ///这里敲重点。java.lang.String 只有转为kotlin.String
                    if (asType.toString()=="java.lang.String"){
                        asType = kotlin.String::class.asTypeName()
                    }
                    val b = ParameterSpec.builder(param.simpleName.toString(), asType)//将参数构建出来
                        .build()
                    params.add(b)
                    if(index==0){
                        returnParams.append(param.simpleName.toString())
                    }else{
                        returnParams.append(",".plus(param.simpleName.toString()))
                    }
                }
            }
            //获取到返回类型
            var asTypeName = it.returnType.asTypeName()

            val build = FunSpec.builder(it.simpleName.toString())
                .addParameters(params)
                .addModifiers(KModifier.PUBLIC,KModifier.SUSPEND)
                .returns(returnName)
                .addStatement(
                    if (asTypeName.toString()=="kotlin.Unit")
                        "mApiService!!.${it.simpleName}(%L)"
                    else
                        "return mApiService!!.${it.simpleName}(%L)"
                    ,returnParams.toString())
                .build()
            hashMap.add(build)
        }
        return hashMap
    }
    fun getPackageName(): String {
        return mPackageName!!
    }
    fun getProxyClassFullName(): String {
        return "$mPackageName.$mClassName"
    }
    fun getClassName(): String {
        return "$mClassName"
    }

3.如何使用
工程结构如下大致


QQ图片20230306184151.png

各自module的gradle文件依赖commom组件。common的gradle文件添加apt依赖,kotlin必须是kapt

    api project(path: ':apt-annotation')
    kapt project(path: ':apt-repository')

接口方法添加注解

RetrofitManager添加自定义注解即可

4.最终apt会帮助我们自动生成kotlin协程方法

我们只需要在viewModel中调用方法

   fun dsad(ctx:Activity){
        viewModelScope.launch{
            //只需要各自module的接口类名(比如ApiBasic)加上 _repository就可以一行代码直接网络请求了
            val result1 =ApiBasic_repository.getInstance().test2("tiyu_new","15","pcFeed","pd")
        }
    }

这样每个负责自己模块的小伙伴就可以负责各自的接口类而不用影响到其他人,也不用单独去写接口实现类了

OVER

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容