前言:由于框架本身也在不断地迭代,因此文章中的部分代码可能存在更新或者过时,如果你想阅读源码或者查看代码的在项目中的实际使用方法,可以查看笔者目前在维护的compose项目:Spacecraft: 《Spacecraft - 我的安卓技术实践平台》-查看代码请进入develop分支 (gitee.com)
本篇内容主要为实现网络框架中与全局相关的逻辑,如果你没有看过上一篇,请跳转至优雅使用Retrofit,在协程时代遨游安卓网络请求(二) - 掘金 (juejin.cn)
网络异常转自然语言信息
在上一节中,我们遗留了部分代码未提供,这里我们回顾一下
/**
* 网络请求出现异常
*/
data class Exception<T>(val exception: Throwable) : Failure<T>() {
//分析异常类型,返回自然语言错误信息
val exceptionMessage:String by lazy {
//TODO 下文讲解
}
}
这个自然语言的意思是:用户能直接阅读的错误信息,我们换个角度想一下,如果程序直接把网络超时异常SocketTimeoutException的信息抛出给用户,例如一个Toast,会发生什么事呢?可想而知用户会感到非常的困惑(前提是他不是一个程序员),因此需要将粗暴的程序错误堆栈信息转换成人可读的信息。
依然是废话不多说!上代码。
/**
* 网络异常解析器
*/
interface HttpExceptionParser{
/**
* 将一个异常转化为自然语言报错
*/
fun parse(throwable: Throwable?): String
/**
* 默认实现
*/
companion object{
val DEFAULT_PARSER= object : HttpExceptionParser {
override fun parse(throwable: Throwable?): String {
return when (throwable) {
is ConnectException, is SocketException, is HttpException, is UnknownHostException -> "网络连接失败"
is SSLHandshakeException -> "证书验证失败"
is JSONException, is ParseException, is JsonParseException -> "解析报文失败"
is SocketTimeoutException -> "连接超时"
is MsgException -> throwable.tip
else -> "未知错误"
}
}
}
}
}
代码非常的简单,就是一个接口中包含了一个方法,将throwable转成字符串,然后接口中包含一个默认实现(这里可以改成你想要的)
接下来,我们再实现一个Holder去持有Parser,让Holder实现Parser的方法(代理者模式),然后调用真正的parser去解析(你可以更换parser去实现你想要的自定义逻辑)
/**
* 异常处理器持有者
*/
object HttpExceptionParserHolder:HttpExceptionParser {
var parser=DEFAULT_PARSER
override fun parse(throwable: Throwable?): String {
return parser.parse(throwable)
}
}
这样,回到最初的代码,我们把Holder传入进去,完成逻辑闭环,用户就再也不会看到奇奇怪怪的网络异常报错了。
/**
* 网络请求出现异常
*/
data class Exception<T>(val exception: Throwable) : Failure<T>() {
val exceptionMessage: String by lazy {
HttpExceptionParserHolder.parse(exception)
}
}
NetworkResult拦截器
在本网络框架的网络请求中,在服务器无异常且拿到服务器报文后,逻辑会执行到Success中去,但是有时候我们有这样的需求,报文中有个字段,例如Code,如果Code不为1,则说明业务报错,因此我们还需要进一步判断Code的值,否则会出现BUG。
那么问题来了,我们如果实现全局逻辑而不是在具体的某个请求的Success中回调呢? 笔者参考过很多人的解决方案,大部分是对OKhttp的拦截器进行处理,即在okhttp拦截器中进行校验,但是这样有个缺陷是,你需要多执行一遍实体类型的转换(因为Retrofit层已经有一层),性能差而且存在代码维护的难度(因为存在两套实体类转换逻辑,okhttp和retrofit各一套),因此我们希望统一在Retrofit层面进行校验。
让我们回到第一节的代码(第一节传送门:优雅使用Retrofit,在协程时代遨游安卓网络请求(一) - 掘金 (juejin.cn))
在第一节中,笔者提供了一个将Retrofit的Response转成NetworkResult的扩展方法,我们的关键点就是这里。
fun <T> Response<T>.toNetworkResult(): NetworkResult<T> =
//此处可以做全局转换
try {
if (isSuccessful) {
toSuccessResult()
} else {
toServerErrorResult()
}
} catch (t: Throwable) {
t.toExceptionResult()
}
这个方法的返回值是NetworkResult,因此可以返回Success,Failure中的任意一种,我们只需要在这里插入转换的逻辑,将部分Success的转成Failure即可(当然你可以实现任意的转换,甚至将错误转成正确都可以,非常的任性)。
那么如何实现的,废话不多说,直接上代码!
interface GlobalNetworkResultInterceptor {
/**
* 拦截网络请求,
*/
fun <T> onIntercept(networkResult: NetworkResult<T>): NetworkResult<T> {
return networkResult
}
//默认实现
object DefaultGlobalNetworkResultInterceptor : GlobalNetworkResultInterceptor
}
和异常解析器非常类似,也是一接口带着一个默认实现,方法就是将一个NetworkResult变成另外一个NetworkResult,默认就是不转。
为了方便读者理解,我们用玩安卓的api来辅助说明,首先玩安卓的api接口返回值全是一个模板(大部分公司都类似),用实体类表示如下:
/**
* 带壳的相应bean
* @param T data实体类
* @property data T 报文中对应data的部分
* @property errorCode Int 报文中对应errorCode的部分
* @property errorMsg String 报文中对应errorMsg的部分
* @constructor
*/
data class WanBeanWrapper<T>(
val data: T,
val errorCode: Int = -1,
val errorMsg: String = ""
) {
companion object {
const val SUCCESS_CODE = 0
const val LOGIN_ERROR_CODE = -1001
}
/**
* 请求是否成功
* @return Boolean true:成功
*/
fun isSuccessful(): Boolean {
return errorCode == SUCCESS_CODE
}
/**
* 登陆失败
* @return Boolean true:登陆失败
*/
fun isLoginError(): Boolean {
return errorCode == LOGIN_ERROR_CODE
}
}
对于全局拦截器来说,只需要关注到这个壳的就够了,假设我们需要实现一个逻辑:当errorCode不等于SUCCESS_CODE的时候,我们需要将Success类型的networkResult转成Error类型的NetworkResult。
来看看笔者在项目中实现的一个拦截器:
class WanGlobalNetworkResultInterceptor @Inject constructor() : GlobalNetworkResultInterceptor {
override fun <T> onIntercept(networkResult: NetworkResult<T>): NetworkResult<T> {
return if (
//只有成功才转换
networkResult is Success &&
//只转换这种类型的Bean
networkResult.responseBody is WanBeanWrapper<*>
) {
//类型强转
val wanBeanWrapper = (networkResult.responseBody as WanBeanWrapper<*>)
//如果不成功
if (!wanBeanWrapper.isSuccessful()) {
return MsgException(wanBeanWrapper.errorMsg).toExceptionResult()
}
networkResult
} else {
networkResult
}
}
}
可以看出完成了NetworkResult类型的转换,一开始是一个Success类型的,然后通过校验报文中的errorCode来发现后台给我们报错了,于是我们基于MsgException(只是一个简单的IoException的子类,笔者用来在okhttp的拦截其中报一些自定义的错误,你可以使用你想要的任意异常来封装ExceptionResult)来生成一个新ExceptionResult。这样,原本会走向成功的逻辑变成走向了错误的逻辑,调用者无需在每一次网络请求成功之后都手动判断errorCode是否正常!
让我们回到第一章的代码,进行少部分的修改。
//扩展方法新增一个参数
fun <T> Response<T>.toNetworkResult(interceptor: GlobalNetworkResultInterceptor): NetworkResult<T> =
interceptor.onIntercept(
try {
if (isSuccessful) {
toSuccessResult()
} else {
toServerErrorResult()
}
} catch (t: Throwable) {
t.toExceptionResult()
}
)
//代理类
internal class ApexResponseCallDelegate<T>(
private val proxyCall: Call<T>,
//新增参数,传入全局拦截器
private val interceptor: GlobalNetworkResultInterceptor
) :
Call<NetworkResult<T>> {
override fun enqueue(callback: Callback<NetworkResult<T>>) =
proxyCall.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(
this@ApexResponseCallDelegate,
Response.success(
//将参数传入到这里使用
response.toNetworkResult(interceptor)
)
)
}
//省略部分代码
})
这样就可以低成本的对原本的代码进行修改,全局返回结果的拦截修改就完成了!
感谢你看到这里,这个简单的系列就到此结束了,如果你有更多疑问可以在评论区给笔者留言,想看源码也可以去笔者的开源项目中找到相关的代码(搜索类名即可,部分类名可能会发生改变)。