稀土掘金技术社区 01月25日
花式封装:Kotlin+协程+Flow+Retrofit+OkHttp+Repository,彻底减少模版代码进阶之路
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了如何利用注解处理器解决Android开发中Repository层的模板代码问题。文章从最常见的网络请求封装模式出发,逐步引入通过注解处理器自动生成Repository类和接口的方法。首先,展示了使用Kotlin、协程、Flow、Retrofit等技术进行网络请求的传统方式,然后通过注解处理器,实现了根据NetApi接口自动生成Repository类,并允许对特殊方法进行自定义处理。最后,进一步通过注解处理器生成INetApiRepository接口,使Repository层的代码更加简洁高效,旨在提高开发效率。

💡 传统网络请求封装:文章首先介绍了基于Kotlin协程、Flow、Retrofit和OkHttp的常见网络请求封装模式,包括NetApi接口定义、Repository数据处理以及ViewModel的调用,展示了这种模式下Repository中存在大量模板代码的问题。

🛠️ 注解处理器自动生成Repository:文章核心内容是利用注解处理器,根据NetApi接口自动生成对应的Repository类,从而避免了手动编写大量重复的模板代码,同时还支持通过@Filter注解标记需要特殊处理的方法,实现定制化的数据处理逻辑。

✨ 自动生成Repository接口:为了进一步简化代码,文章还介绍了如何通过注解处理器自动生成INetApiRepository接口,该接口的定义与NetApi保持一致,只是返回类型变为了Flow,使得Repository层的数据流处理更为统一和简洁。

原创 Wgllss 2025-01-23 08:31 重庆

点击关注公众号,“技术干货” 及时达!

点击关注公众号,“技术干货” 及时达!

前言 :众里寻它千百度, 蓦然回首,此种代码却在灯火阑珊处。

注解处理器在架构,框架中实战应用:MVVM中数据源提供Repository类的自动生成

一、前言

    本文介绍思路:
    本文重点介绍思路:四种方式花式解决Repository中模版式的代码,逐级递增
    1.1 :涉及到Kotlin协程Flow、viewModel、Retrofit、Okhttp相关用法
    1.2 :涉及到注解反射泛型注解处理器相关用法
    1.3 :涉及到动态代理kotlinsuspend方法反射调用及反射中异常处理
    1.4 :本示例4个项目如图:


    网络框架搭建的封装,到目前为止最为流行又很优雅的的是 Kotlin+协程+Flow+Retrofit+OkHttp+Repository

    先来看看中间各个类的职责:

    从上图可以看出单一职责:

    NetApi: 负责网络接口配置,包括 请求地址,请求头,请求方式,参数等等所有配置

    Flow+Retrofit+Okhttp: 联合起来负责把 NetApi 中的各种配置组装成网络请求行为,并且通过Flow 组装成流,通过它可以控制该行为的异步方式,异步开始结束等等一系列的流行为。

    Repository: 负责 Flow+Retrofit+Okhttp 请求结果的数据流,进行加工处理成我们想要的数据,大多数不需要处理的,可以直接给到 ViewModel

    ViewModel: 负责调用 Repository,拿到想要的数据然后提供给UI方展示使用或者相关使用

    也可以看到 它的 持有链 从右向左 一条线性持有:ViewModel 持有 RepositoryRepository持有 Flow+Retrofit+Okhttp ,Flow+Retrofit+Okhttp 持有 NetApi

    最终我们可以得到:
    5.1. 网络请求行为 会根据 NetApi 写出模板式的代码,这块解决模版式的代码在 Retrofit 中它通过动态代理,把所有模版式的代码统一成了一个
    5.2. 同理:Repository 也是根据 NetApi 配置的接口,写成模版式的代码转换成流

二、花式封装(一)

    NetApi 的配置:

<!---->
interface NetApi {
// 示例get 请求 @GET("https://www.wanandroid.com/article/list/0/json") suspend fun getHomeList(): CommonResult<WanAndroidHome>
// 示例get 请求2 @GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList(@Path("path") page: Int): CommonResult<WanAndroidHome>
@GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList(@Path("path") page: Int, @Path("path") a: Int): CommonResult<WanAndroidHome>
@GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList(@Path("path") page: Int, @Path("path") f: Float): CommonResult<WanAndroidHome>
// 示例get 请求2 @GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList2222(@Path("path") page: Int): CommonResult<WanAndroidHome>
@GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList3333(@Path("path") page: Int): CommonResult<WanAndroidHome>
@GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList5555(@Path("path") page: Int, @Query("d") ss: String, @HeaderMap map: Map<String, String>): CommonResult<WanAndroidHome>
@GET("https://www.wanandroid.com/article/list/{path}/json") suspend fun getHomeList6666( @Path("path") page: Int, @Query("d") float: Float, @Query("d") long: Long, @Query("d") double: Double, @Query("d") byte: Byte, @Query("d") short: Short, @Query("d") char: Char, @Query("d") boolean: Boolean, @Query("d") string: String, @Body body: RequestBodyWrapper ): CommonResult<WanAndroidHome> //示例post 请求 @FormUrlEncoded @POST("https://www.wanandroid.com/user/register") suspend fun register( @Field("username") username: String, @Field("password") password: String, @Field("repassword") repassword: String ): String /************************* 以下只 示例写法,接口调不通,因为找不到那么多 公开接口 全是 Retrofit的用法 来测试 *****************************************************/

// @FormUrlEncoded @Headers("Content-Type: application/x-www-form-urlencoded") //todo 固定 header @POST("https://xxxxxxx") suspend fun post1(@Body body: RequestBody): String
// @FormUrlEncoded @Headers("Content-Type: application/x-www-form-urlencoded") @POST("https://xxxxxxx22222") suspend fun post12(@Body body: RequestBody, @HeaderMap map: Map<String, String>): String //todo HeaderMap 多个请求头部自己填写
suspend fun post1222(@Body body: RequestBody, @HeaderMap map: Map<String, Any>): String //todo HeaderMap 多个请求头部自己填写 }

2.  NetRepository 中是 根据 NetApi 写出下面类似的全模版式的代码:都是返回 Flow

<!---->
class NetRepository private constructor() { val service by lazy { RetrofitUtils.instance.create(NetApi::class.java) }
companion object { val instance by lazy { NetRepository() } }
// 示例get 请求 fun getHomeList() = flow { emit(service.getHomeList()) }
// 示例get 请求2 fun getHomeList(page: Int) = flow { emit(service.getHomeList(page)) }
fun getHomeList(page: Int, a: Int) = flow { emit(service.getHomeList(page, a)) }
fun getHomeList(page: Int, f: Float) = flow { emit(service.getHomeList(page, f)) }
// 示例get 请求2 fun getHomeList2222(page: Int) = flow { emit(service.getHomeList2222(page)) }
fun getHomeList3333(page: Int) = flow { emit(service.getHomeList3333(page)) }
fun getHomeList5555(page: Int, ss: String, map: Map<String, String>) = flow { emit(service.getHomeList5555(page, ss, map)) }
fun getHomeList6666( page: Int, float: Float, long: Long, double: Double, byte: Byte, short: Short, char: Char, boolean: Boolean, string: String, body: RequestBodyWrapper ) = flow { emit(service.getHomeList6666(page, float, long, double, byte, short, char, boolean, string, body)) }
fun register(username: String, password: String, repassword: String) = flow { emit(service.register(username, password, repassword)) }
// // /************************* 以下只 示例写法,接口调不通,因为找不到那么多 公开接口 全是 Retrofit的用法 来测试 *****************************************************/ // // fun post1(body: RequestBody) = flow { emit(service.post1(body)) }
fun post12(body: RequestBody, map: Map<String, String>) = flow { emit(service.post12(body, map)) }
fun post1222(id: Long, asr: String) = flow { val map = mutableMapOf<String, Any>() map["id"] = id map["asr"] = asr val mapHeader = HashMap<String, Any>() mapHeader["v"] = 1000 mapHeader["device_sn"] = "Avidfasfa1213" emit(service.post1222(RequestBodyWrapper(Gson().toJson(map)), mapHeader)) } }

3.  viewModel 调用端:

<!---->
class MainViewModel : BaseViewModel() {
private val repository by lazy { NetRepository.instance }
fun getHomeList(page: Int) { flowAsyncWorkOnViewModelScopeLaunch { repository.getHomeList(page).onEach { android.util.Log.e("MainViewModel", "one 111 ${it.data?.datas!![0].title}") } } } }

—————————————————我是分割线君—————————————————

    上面花式玩法(一): 此种写法被广泛称作 最优雅的一套网络封装 框架,

    绝大多数中、大厂 基本也就封装到此为止了

    可能还有些人想着:你的 repository 中就返回了 Flow<T> , 里面就全是简单的 emit(xxx) ,我项目里面不是这样的,我的还封装了成功,失败,或者其他的,但总体还是全是模版式的,除了特殊的一些方法,需要在请求前 ,请求后做些处理,有规律有模版的还是占大多数吧,只要大多数都一样的规律模版,都是可以处理的,里面稍微修改下细节,思路都是一样的。

    哪还能有什么玩法?

    可能会有人想到 借助 HiltDagger2Koin 来创建 Retrofit,和创建 repository,创建 ViewModel这里不是讨论依赖注入创建对象的事情

    哪还有什么玩法?

    有,必须有的。

三、花式封装(二)

    既然上面是 Repository 类中,所有写法都是固定模版式的代码,那么让其根据 NetApi: 自动生成 Repository 类,我们这里借用注解处理器。

    具体怎么使用介绍,请参考:
    注解处理器在架构,框架中实战应用:MVVM中数据源提供Repository类的自动生成

    本项目中只需要编译   app_wx2 工程

    在下图中找到

5. viewModel调用端

    class MainViewModel : BaseViewModel() {
private val repository by lazy { RNetApiRepository() }
fun getHomeList(page: Int) { flowAsyncWorkOnViewModelScopeLaunch { val time = System.currentTimeMillis() repository.getHomeList(page).onEach { android.util.Log.e("MainViewModel", "two 222 ${it.data?.datas!![0].title}") android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms") } } } }

6.  如果 Repository 中某个接口方法需要特殊处理怎么办?比如下图,请求前处理一下,从 拿到数据后我需要再次转化处理之后再给到 viewModel 怎么办?

<!---->
//我这个接口 ,请求前需要 判断处理一下,拿到数据后也需要再处理一下 fun post333(id: Long, asr: String, m: String, n: String, list: List<String>) = flow { val map = mutableMapOf<String, Any>() map["id"] = id map["asr"] = asr val mapHeader = HashMap<String, Any>() mapHeader["v"] = 1000 mapHeader["device_sn"] = "Avidfasfa1213"
//接口调用前 根据 需要处理操作 list.forEach { if (map.containsKey(id.toString())) { /// } }
val result = service.post1222(RequestBodyWrapper(Gson().toJson(map)), mapHeader) // 拿到数据后需要处理操作 val result1 = result emit(result1) }.map { //需要再转化一下 it }.filter { //过滤一下 it.length == 3 }

7.  可以在 接口 NetApi 中该方法上配置 @Filter 注解过滤 ,该方法需要自己特殊处理,不自动生成,如下


@Filter@POST("https://xxxxxxx22222")suspend fun post333(@Body body: RequestBody, @HeaderMap map: Map<String, Any>): String

    如果想 post请求的 RequestBody 内部参数单独出来进入方法传参,可以加上 在 NetApi 中方法加上 @PostBody:如下:

<!---->
@PostBody("{"ID":"Long","name":"String"}") @POST("https://www.wanandroid.com/user/register") suspend fun testPostBody222(@Body body: RequestBody): String

这样 该方法生成出来的对应方法就是:

    public suspend fun testPostBody222(ID: Long, name: java.lang.String): Flow<String> =        kotlinx.coroutines.flow.flow {            val map = mutableMapOf<String, Any>()            map["ID"] = ID            map["name"] = name            val result = service.testPostBody222(com.wx.test.api.retrofit.RequestBodyCreate.toBody(com.google.gson.Gson().toJson(map)))            emit(result)        }

怎么特殊处理,单独手动建一个Repository,针对该方法,单独写,特殊就要特殊手动处理,但是大多数模版式的代码,都可以让其自动生成。

—————————————————我是分割线君—————————————————

到了这里,我们再想, NetApi 是一个接口类,
但是实际上没有写接口实现类啊, 它怎么实现的呢?
我们上面 花式玩法(二) 中虽然是自动生成的,但是还是有方法体,

可不可以再省略点?

可以,必须有!

四、花式玩法(三)

    我们可以根据 NetApi 里面的配置,自动生成 INetApiRepository 接口类,接口名和参数 都和 NetApi 保持一致,唯一区别就是返回的对象变成了 Flow<T> 了,
    这样在 Repository 中就把数据转变为 flow  流了

    配置让代码自动生成的类:

<!---->
@AutoCreateRepositoryInterface(interfaceApi = "com.wx.test.api.net.NetApi") class KaptInterface { }

生成的接口类 INetApiRepository 代码如下:


public interface INetApiRepository { public fun getHomeList(): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList(page: Int): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList(page: Int, f: Float): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList(page: Int, a: Int): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList2222(page: Int): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList3333(page: Int): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList5555( page: Int, ss: String, map: Map<String, String> ): Flow<CommonResult<WanAndroidHome>>
public fun getHomeList6666( page: Int, float: Float, long: Long, double: Double, byte: Byte, short: Short, char: Char, boolean: Boolean, string: String, body: RequestBodyWrapper ): Flow<CommonResult<WanAndroidHome>>
public fun getHomeListA(page: Int): Flow<Call<HomeData>>
public fun getHomeListB(page: Int): Flow<HomeData>
public fun post1(body: RequestBody): Flow<String>
public fun post12(body: RequestBody, map: Map<String, String>): Flow<String>
public fun post1222(body: RequestBody, map: Map<String, Any>): Flow<String>
public fun register( username: String, password: String, repassword: String ): Flow<String>
public fun testPostBody222(ID: Long, name: java.lang.String): Flow<String>}

    Repository 职责承担的调用端:用动态代理:


class RepositoryPoxy private constructor() : BaseRepositoryProxy() {
val service = NetApi::class.java val api by lazy { RetrofitUtils.instance.create(service) }

companion object { val instance by lazy { RepositoryPoxy() } }
fun <R> callApiMethod(serviceR: Class<R>): R { return Proxy.newProxyInstance(serviceR.classLoader, arrayOf(serviceR)) { proxy, method, args -> flow { val funcds = findSuspendMethod(service, method.name, args) if (args == null) { emit(funcds?.callSuspend(api)) } else { emit(funcds?.callSuspend(api, *args)) }// emit((service.getMethod(method.name, *parameterTypes)?.invoke(api, *(args ?: emptyArray())) as Call<HomeData>).execute().body()) }.catch { if (it is InvocationTargetException) { throw Throwable(it.targetException) } else { it.printStackTrace() throw it } } } as R }}

    BaseRepositoryProxy 中内容:


open class BaseRepositoryProxy {
private val map by lazy { mutableMapOf<String, KFunction<*>?>() } private val sb by lazy { StringBuffer() }
@OptIn(ExperimentalStdlibApi::class) fun <T : Any> findSuspendMethod(service: Class<T>, methodName: String, args: Array<out Any>): KFunction<*>? { sb.delete(0, sb.length) sb.append(service.name) .append(methodName) args.forEach { sb.append(it.javaClass.typeName) } val key = sb.toString() if (!map.containsKey(key)) { val function = service.kotlin.memberFunctions.find { f -> var isRight = 0 if (f.name == methodName && f.isSuspend) { if (args.size == 0 && f.parameters.size == 1) { isRight = 2 } else { f.parameters.forEachIndexed { index, it -> if (index > 0 && args.size > 0) { if (args.size == 0) { isRight = 2 return@forEachIndexed } if (it.type.javaType.typeName == javaClassTransform(args[index - 1].javaClass).typeName) { isRight = 2 } else { isRight = 1 return@forEachIndexed } } } } } //方法名一直 是挂起函数 方法参数个数一致, 参数类型一致 f.name == methodName && f.isSuspend && f.parameters.size - 1 == args.size && isRight == 2 } map[key] = function } return map[key] }
private fun javaClassTransform(clazz: Class<Any>) = when (clazz.typeName) { "java.lang.Integer" -> Int::class.java "java.lang.String" -> String::class.java "java.lang.Float" -> Float::class.java "java.lang.Long" -> Long::class.java "java.lang.Boolean" -> Boolean::class.java "java.lang.Double" -> Double::class.java "java.lang.Byte" -> Byte::class.java "java.lang.Short" -> Short::class.java "java.lang.Character" -> Char::class.java "SingletonMap" -> Map::class.java "LinkedHashMap" -> MutableMap::class.java "HashMap" -> HashMap::class.java "Part" -> MultipartBody.Part::class.java "RequestBody" -> RequestBody::class.java else -> { if ("RequestBody" == clazz.superclass.simpleName) { RequestBody::class.java } else { Any::class.java } } }}

    ViewModel中调用端:

<!---->
class MainViewModel : BaseViewModel() {
private val repository by lazy { RepositoryPoxy.instance }
fun getHomeList(page: Int) { flowAsyncWorkOnViewModelScopeLaunch { val time = System.currentTimeMillis() repository.callApiMethod(INetApiRepository::class.java).getHomeList(page).onEach { android.util.Log.e("MainViewModel", "three 333 ${it.data?.datas!![0].title}") android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms") } } } }

    上面生成的接口类 INetApiRepository 其实方法和 NetApi 拥有相似的模版,唯一区别就是返回类型,一个是对象,一个是Flow流的对象

    还能省略吗?

    有,必须有

五、花式玩法(四)

    直接修改  RepositoryPoxy ,作为Reposttory的职责 ,连上面的 INetApiRepository 的接口类全部省略了, 如下:

<!---->
class RepositoryPoxy private constructor() : BaseRepositoryProxy() {
val service = NetApi::class.java val api by lazy { RetrofitUtils.instance.create(service) }

companion object { val instance by lazy { RepositoryPoxy() } }
fun <R> callApiMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow<R> { return flow { val clssss = mutableListOf<Class<out Any>>() args?.forEach { clssss.add(javaClassTransform(it.javaClass)) } val parameterTypes = clssss.toTypedArray() val call = (service.getMethod(methodName, *parameterTypes)?.invoke(api, *(args ?: emptyArray())) as Call<R>) call?.execute()?.body()?.let { emit(it as R) } } }
@OptIn(ExperimentalStdlibApi::class) fun <R> callApiSuspendMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow<R> { return flow { val funcds = findSuspendMethod(service, methodName, args) if (args == null) { emit(funcds?.callSuspend(api) as R) } else { emit(funcds?.callSuspend(api, *args) as R) } } } }

2.  ViewModel中调用入下:

<!---->
class MainViewModel : BaseViewModel() {
private val repository by lazy { RepositoryPoxy.instance }
fun getHomeList(page: Int) { flowAsyncWorkOnViewModelScopeLaunch { val time = System.currentTimeMillis() repository.callApiSuspendMethod(HomeData::class.java, "getHomeListB", page).onEach { android.util.Log.e("MainViewModel", "four 444 ${it.data?.datas!![0].title}") android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms") } } } }

六、总结

通过上面4中花式玩法:

    花式玩法1: 我们知道了最常见最优雅的写法,但是模版式 repository 代码太多,而且需要手动写

    花式玩法2: 把花式玩法1中的模版式 repository ,让其自动生成,对于特殊的方法,单独手动再写个 repository ,这样让大多数模版式代码全自动生成

    花式玩法3NetApi,可以根据配置,动态代理生成网络请求行为,该行为统一为动态代理实现,无需对接口类 NetApi 单独实现,那么我们的 repository 也可以 生成一个接口类 INetApiRepository ,然后动态代理实现其内部 方法体逻辑

    花式玩法4:我连花式玩法3中的接口类 INetApiRepository 都不需要了,直接反射搞定所有。

    同时可以学习到,注解、反射、泛型、注解处理器、动态代理

项目地址

项目地址:
github地址
gitee地址

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

注解处理器 Repository 代码生成 Android开发 网络请求
相关文章