注册

花式封装:Kotlin+协程+Flow+Retrofit+OkHttp +Repository,倾囊相授,彻底减少模版代码进阶之路

e1ff3706ea196f758818da129df6de53.png

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

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

一、前言

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

    380Xt8NSYZ.jpg

  2. 网络框架搭建的封装,到目前为止最为流行又很优雅的的是 Kotlin+协程+Flow+Retrofit+OkHttp+Repository
  3. 先来看看中间各个类的职责: whiteboard_exported_image.png
  4. 从上图可以看出单一职责:

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

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

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

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

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

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

二、花式封装(一)

  1. NetApi 的配置:
interface NetApi {

// 示例get 请求
@GET("https://www.wanandroid.com/article/list/0/json")
suspend fun getHomeList(): CommonResult

// 示例get 请求2
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int): CommonResult

@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int, @Path("path") a: Int): CommonResult

@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int, @Path("path") f: Float): CommonResult

// 示例get 请求2
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList2222(@Path("path") page: Int): CommonResult

@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList3333(@Path("path") page: Int): CommonResult

@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList5555(@Path("path") page: Int, @Query("d") ss: String, @HeaderMap map: Map): CommonResult

@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

//示例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 //todo HeaderMap 多个请求头部自己填写

suspend fun post1222(@Body body: RequestBody, @HeaderMap map: Map): 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()
map["id"] = id
map["asr"] = asr
val mapHeader = HashMap()
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}")
}
}
}
}

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

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

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

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

    哪还能有什么玩法?

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

    哪还有什么玩法?

    有,必须有的。

三、花式封装(二)

  1. 既然上面是 Repository 类中,所有写法都是固定模版式的代码,那么让其根据 NetApi: 自动生成 Repository 类,我们这里借用注解处理器。
  2. 具体怎么使用介绍,请参考:
    注解处理器在架构,框架中实战应用:MVVM中数据源提供Repository类的自动生成
  3. 本项目中只需要编译 app_wx2 工程
  4. 在下图中找到

img_v3_02f0_d5bd4278-53ac-4008-aac2-abcfdf81668g.jpg 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()
map["id"] = id
map["asr"] = asr
val mapHeader = HashMap()
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
,>
  1. 如果想 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 =
kotlinx.coroutines.flow.flow {
val map = mutableMapOf()
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 是一个接口类,
但是实际上没有写接口实现类啊, 它怎么实现的呢?
我们上面 花式玩法(二) 中虽然是自动生成的,但是还是有方法体,

可不可以再省略点?

可以,必须有!

四、花式玩法(三)

  1. 我们可以根据 NetApi 里面的配置,自动生成 INetApiRepository 接口类, 接口名和参数 都和 NetApi 保持一致,唯一区别就是返回的对象变成了 Flow 了,
    这样在 Repository 中就把数据转变为 flow 流了
  2. 配置让代码自动生成的类:
@AutoCreateRepositoryInterface(interfaceApi = "com.wx.test.api.net.NetApi")
class KaptInterface {
}

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


public interface INetApiRepository {
public fun getHomeList(): Flow>

public fun getHomeList(page: Int): Flow>

public fun getHomeList(page: Int, f: Float): Flow>

public fun getHomeList(page: Int, a: Int): Flow>

public fun getHomeList2222(page: Int): Flow>

public fun getHomeList3333(page: Int): Flow>

public fun getHomeList5555(
page: Int,
ss: String,
map: Map<String, String>
)
: Flow>

public fun getHomeList6666(
page: Int,
float: Float,
long: Long,
double: Double,
byte: Byte,
short: Short,
char: Char,
boolean: Boolean,
string: String,
body: RequestBodyWrapper
)
: Flow>

public fun getHomeListA(page: Int): Flow>

public fun getHomeListB(page: Int): Flow

public fun post1(body: RequestBody): Flow

public fun post12(body: RequestBody, map: Map<String, String>): Flow

public fun post1222(body: RequestBody, map: Map<String, Any>): Flow

public fun register(
username: String,
password: String,
repassword: String
)
: Flow

public fun testPostBody222(ID: Long, name: java.lang.String): Flow
}
  1. 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 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).execute().body())
}.catch {
if (it is InvocationTargetException) {
throw Throwable(it.targetException)
} else {
it.printStackTrace()
throw it
}
}
} as R
}
}
  1. BaseRepositoryProxy 中内容:

open class BaseRepositoryProxy {

private val map by lazy { mutableMapOf?>() }
private val sb by lazy { StringBuffer() }

@OptIn(ExperimentalStdlibApi::class)
fun 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
}
}
}
}
,>
  1. 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")
}
}
}
}

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

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

    还能省略吗?

    有,必须有

五、花式玩法(四)

  1. 直接修改 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 callApiMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow {
return flow {
val clssss = mutableListOfout Any>>()
args?.forEach {
clssss.add(javaClassTransform(it.javaClass))
}
val parameterTypes = clssss.toTypedArray()
val call = (service.getMethod(methodName, *parameterTypes)?.invoke(api, *(args ?: emptyArray())) as Call)
call?.execute()?.body()?.let {
emit(it as R)
}
}
}

@OptIn(ExperimentalStdlibApi::class)
fun callApiSuspendMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow {
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. 花式玩法1: 我们知道了最常见最优雅的写法,但是模版式 repository 代码太多,而且需要手动写
  2. 花式玩法2: 把花式玩法1中的模版式 repository ,让其自动生成,对于特殊的方法,单独手动再写个 repository ,这样让大多数模版式代码全自动生成
  3. 花式玩法3: NetApi,可以根据配置,动态代理生成网络请求行为,该行为统一为动态代理实现,无需对接口类 NetApi 单独实现,那么我们的 repository 也可以 生成一个接口类 INetApiRepository ,然后动态代理实现其内部 方法体逻辑
  4. 花式玩法4:我连花式玩法3中的接口类 INetApiRepository 都不需要了,直接反射搞定所有。
  5. 同时可以学习到,注解、反射、泛型、注解处理器、动态代理

项目地址

项目地址:
github地址
gitee地址

感谢阅读:

欢迎 点赞、收藏、关注


作者:Wgllss
来源:juejin.cn/post/7417847546323042345

0 个评论

要回复文章请先登录注册