花式封装:Kotlin+协程+Flow+Retrofit+OkHttp+Repository,彻底减少模版代码进阶之路
前言 :众里寻它千百度, 蓦然回首,此种代码却在灯火阑珊处。
注解处理器在架构,框架中实战应用:MVVM中数据源提供Repository类的自动生成
一、前言
本文介绍思路:
本文重点介绍思路:四种方式花式解决Repository中模版式的代码,逐级递增
1.1 :涉及到Kotlin、协程、Flow、viewModel、Retrofit、Okhttp相关用法
1.2 :涉及到注解、反射、泛型、注解处理器相关用法
1.3 :涉及到动态代理,kotlin中suspend方法反射调用及反射中异常处理
1.4 :本示例4个项目如图:网络框架搭建的封装,到目前为止最为流行又很优雅的的是
Kotlin+协程+Flow+Retrofit+OkHttp+Repository先来看看中间各个类的职责:
从上图可以看出
单一职责:NetApi:负责网络接口配置,包括 请求地址,请求头,请求方式,参数等等所有配置Flow+Retrofit+Okhttp:联合起来负责把NetApi中的各种配置组装成网络请求行为,并且通过Flow 组装成流,通过它可以控制该行为的异步方式,异步开始结束等等一系列的流行为。Repository:负责Flow+Retrofit+Okhttp请求结果的数据流,进行加工处理成我们想要的数据,大多数不需要处理的,可以直接给到ViewModelViewModel:负责调用Repository,拿到想要的数据然后提供给UI方展示使用或者相关使用也可以看到 它的 持有链 从右向左 一条线性持有:
ViewModel持有Repository,Repository持有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 请求2fun 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 请求2fun 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"] = idmap["asr"] = asrval mapHeader = HashMap<String, Any>()mapHeader["v"] = 1000mapHeader["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),我项目里面不是这样的,我的还封装了成功,失败,或者其他的,但总体还是全是模版式的,除了特殊的一些方法,需要在请求前 ,请求后做些处理,有规律有模版的还是占大多数吧,只要大多数都一样的规律模版,都是可以处理的,里面稍微修改下细节,思路都是一样的。哪还能有什么玩法?
可能会有人想到 借助
Hilt,Dagger2,Koin来创建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"] = idmap["asr"] = asrval mapHeader = HashMap<String, Any>()mapHeader["v"] = 1000mapHeader["device_sn"] = "Avidfasfa1213"//接口调用前 根据 需要处理操作list.forEach {if (map.containsKey(id.toString())) {///}}val result = service.post1222(RequestBodyWrapper(Gson().toJson(map)), mapHeader)// 拿到数据后需要处理操作val result1 = resultemit(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"] = IDmap["name"] = nameval 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.javaval 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 = 0if (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 = 2return@forEachIndexed}if (it.type.javaType.typeName == javaClassTransform(args[index - 1].javaClass).typeName) {isRight = 2} else {isRight = 1return@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.javaelse -> {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.javaval 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,这样让大多数模版式代码全自动生成花式玩法3: NetApi,可以根据配置,动态代理生成网络请求行为,该行为统一为动态代理实现,无需对接口类NetApi单独实现,那么我们的repository也可以 生成一个接口类INetApiRepository,然后动态代理实现其内部 方法体逻辑花式玩法4:我连花式玩法3中的接口类 INetApiRepository都不需要了,直接反射搞定所有。同时可以学习到,注解、反射、泛型、注解处理器、动态代理
项目地址
项目地址:
github地址
gitee地址