携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
本文主要分享在实际使用中Retrofit+协程的结果处理的方式
问题
在使用Retrofit的协程接口时,总有一大堆重复且必要的代码。用过协程的朋友们都知道,协程的错误基本上都是使用抛出异常来实现的,Retrofit也不例外。一个普通的请求接口定义如下:
public interface MyApi{
suspend fun getUsers():BaseResponse<List<User>>
}
(PS:BaseResponse为服务器固定格式的包装类)
调用该请求的代码如下:
suspend fun getUsers():List<User>{
try{
val response = myApi.getUsers() //①
if(response.code == 1){//假设code为1为成功,其他为失败
return response.data
}else{
return emptyList()
}
}catch(e:Exception){
return emptyLise()
}
}
上述代码中,绝大部分的代码都是重复性的,也就是说每当我们增加接口的时候,都需要CV重复的代码。其实说到底,只有实际调用MyApi具体方法的这个地方(即①)有所区别,其他的判断code以及try-catch代码块都是相同的。作为一个有追求的码农,我们当然不允许这样的情况出现!
封装方案
首当其冲的就是try-catch,那简单参考Result的做法,定义一个方法,接受一个代码块,并将该代码块放入try中执行。说干就干!
fun <T> launch(block:() -> T):T{
return try{
block()
}catch(e:Exception){
T() //①
}
}
骚等!这个T可咋搞,这可不能直接创建啊!(即①)
好家伙!我不要这个泛型了,直接改成User的list算了!
这时候我同事看不下去了:“怎么能这样呢?又不是什么接口都是查User的!”
我:“我早就想到了,改成List不就完事了吗?”
同事:“去你的!敢情你所有接口都请求一个列表啊?”
我:“即使不是请求列表的接口我也给弄成列表,只是单元素的列表而已,取的时候取列表的第一个元素不就是结果了吗?”
同事:“……”
好吧以上纯属开玩笑。事到如今,为了能够保证这个方法返回的类一致,Result当仁不让地站了出来。
fun <T> launch(block:() -> T):Result<T>{
return try{
Result.success(block())
}catch(e:Exception){
Result.failure(e)
}
}
八卦的同事又过来瞄了一眼,不屑地说:“try-catch加Result,不是早就有了吗?合体进化!出来吧!就决定是你了!runCatching……”
这时候我打断了同事的腿,呸!同事的话:“你还是太小看我了!我当然知道有runCatching,我不用是有自己的想法的。”
原本我就是使用Result配套的runCatching的,直到我遇到了某一个接口——上传文件。为了调试方便,Retrofit的日志打印等级一直是BODY级别。那就造成了当我在调试上传文件这个接口的时候,AS的Logcat被塞爆的情况。当然有很多方法解决这个问题,比如把日志的buffer调大等等,但我其实并不关心文件的内容,所以我选择把打印的等级调成HEADERS。
但是又出现了新的问题,在发起上传请求之前,手动把日志等级调成HEADERS,那我往后的其他需要关心内容的接口不也看不到了?
同事:“请求成功之后调回来就好啦!”
我:“那如果上传失败走到catch里面呢?”
同事:“那在catch里面也加上调日志的代码咯。”
我:“……”
讲到这里,下一位角色应该也呼之欲出了,它就是:finally。
不管有没有catch,我都得把日志等级调回去,这不正是final的使用场景吗?
奈何Result并没有配套finally相关的方法,这也是我放弃了runCatching的原因。
加上final的逻辑后,launch方法如下:
fun <T> launch(
final: () -> Unit = {},
block: () -> T
): Result<T> {
return try {
Result.success(block())
} catch (e: Exception) {
Result.failure(e)
} finally {
final()
}
}
launch方法接受一个无参无返回的方法final,以及一个无参返回T的方法block。final放在前面并且默认值为空方法,因为大部分接口都不需要使用final,因为Kotlin的语法特性,block在最后使得在使用上可以写成Lambda形式。
调用如下:
fun getUsers():Result<List<User>>{
return launch{
val response = myApi.getUsers()
if(response.code == 1){
return response.data
}else{
return emptyList
}
}
}
去掉了try-catch,接下来受刑的就是判断code了。code是BaseResponse的内在属性,判断放在其内部是非常合理的,于是乎:
private const val SUCCESS_CODE = 100000
data class BaseResponse<T>(val code: Int, val message: String, val data: T) {
fun getResult(): Result<T> {
return if (code == SUCCESS_CODE) {
Result.success(data)
} else {
Result.failure(Exception("$message:$code"))
}
}
}
launch方法进一步修改成如下代码:
suspend fun <T> launch(
final: () -> Unit = {},
block: suspend () -> BaseResponse<T>
): Result<T> {
return try {
block().getResult()
} catch (e: Exception) {
Result.failure(e)
} finally {
final()
}
}
block的返回值从T改成了BaseResponse,try里面调用BaseResponse的getResult方法获取结果。
因为使用了Retrofit的协程方法,所以block需要是挂起方法,同时launch也需要是挂起方法。由于Retrofit的协程调用已经在内部实现了线程切换,所以无需再手动切换线程了。
那么launch最终的调用就简化成这样了:
fun getUsers():Result<List<User>> = launch{ myApi.getUsers() }
如此这般,就将一开始繁复的网络请求简化成一行代码。而且使用Result来返回结果对于处理异常也非常方便。
RetrofitManager.getUsers()
.onSuccess{}
.onFailure{}
Result的设计也非常精妙,以后有时间解读一下。
疑问
虽然说这样封装很棒(至少我自己是这么觉得的),但在下载文件的情况下就无能为力了。Retrofit的下载文件需要在API中定义一个返回值为Response的方法。收到回复后调用body获取输入流,后续进行写入文件的操作。这其中并不涉及BaseResponse这个类,所以无法使用launch方法。有解决方法的朋友们欢迎评论指导一下哦。
最后
当然本文的做法只是浅浅地封装,更多的只是我在实际开发中的小技巧,不敢与其他真正封装Retrofit的大佬比肩。只要有人能有收获那就行了。最后当然欢迎友善交流,拒绝开喷!