优雅使用Retrofit,在协程时代遨游安卓网络请求(三)

2,029 阅读6分钟

前言:由于框架本身也在不断地迭代,因此文章中的部分代码可能存在更新或者过时,如果你想阅读源码或者查看代码的在项目中的实际使用方法,可以查看笔者目前在维护的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)
                    )
                )
            }

            //省略部分代码

        })
        

这样就可以低成本的对原本的代码进行修改,全局返回结果的拦截修改就完成了!

感谢你看到这里,这个简单的系列就到此结束了,如果你有更多疑问可以在评论区给笔者留言,想看源码也可以去笔者的开源项目中找到相关的代码(搜索类名即可,部分类名可能会发生改变)。