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

Retrofit框架之美

  在众多安卓网络请求框架中,Retrofit无疑是最耀眼的那个:基于okhttp实现,迁移成本低、API设计简洁易用、注解化配置高度解耦、支持多种解析器、支持rxjava和协程。。。

  然而实际项目中,Retrofit有诸多不便,例如:返回值Call容易陷入嵌套地域、网上大多数CallAdapter方案需要手动try-catch、缺乏全局逻辑等。不过好消息是,得益于Retrofit强大的定制功能,我们可以逐一解决上诉的痛点,让Retrofit从一个花瓶变成一个具有实战意义的工具。

我们的目标是让Retrofit拥有以下功能和特性:

  • 在协程下执行网络请求,让异步请求代码变成近似同步代码,消除嵌套地狱
  • 网络API的注解自定义和逻辑自定义
  • 增加可配置的全局结果转换器,例如返回的成功报文中,含有code为非0的结果,需要转换成错误的报文
  • 网络请求方法不用手动try-catch异常,即方法不向上抛出,方法返回值包含网络异常

阅读本系列需要以下知识点:

  • kotlin
  • Hilt(最好了解)
  • Retrofit(基础或者使用过即可)
  • okhttp(了解拦截器的使用)

test4.png

一:分析问题

  默认情况下,Retrofit支持Call的返回值,我们需要调用Call类中的方法来请求网络,然后通过回调来获取网络请求的成功和失败的结果。

    interface IGetRequest {
        @GET("xxxxxx")
        fun getStudent(): Call<StudentBean>
    }


    call.enqueue(object : Callback<User> {
            override fun onResponse(call: Call<User>, response: Response<User>) {
            }
 
            override fun onFailure(call: Call<User>, t: Throwable) {
            }
        })
    }
复制代码

  这个显然是不符合我们的要求的,因为我们要消除嵌套地狱,我们希望Retrofit直接把网络请求的“结果”返回给调用者,即整个请求是“阻塞”的,即在协程作用域中“挂起”。

  需要注意的一点是,这里的“结果”并不是指网络请求成功的结果,而是指网络请求结束的结果,即结果包含了“成功”、“异常”、“服务器内部错误”三种情况,我们只需要判断结果的类型即可

  因此,大致需要设计一个这样的api。


interface IGetRequest {
        @GET("xxxxxx")
        suspend fun getStudent(): Result<StudentBean>
}

//getStudent()是异步请求,但是在协程作用域中挂起了,因此看起来像是同步代码
val result=iGetRequest.getStudent()

复制代码

二:定义NetworkResult

  废话不多说,先看代码(实体类内部隐藏了部分逻辑代码,例如如何处理response成员属性,会在下篇文章中讲解,本篇重点不在此处因此省略)。

sealed class NetworkResult<T> {

    /**
     * 网络请求成功
     */
    class Success<T>(private val response: Response<T>) : NetworkResult<T>(){}
    
    /**
     * 网络请求失败
     */
    sealed class Failure<T> : NetworkResult<T>() {
        /**
         * 服务器内部错误
         */
        data class ServerError<T>(private val response: Response<T>) : Failure<T>() {}

        /**
         * 网络请求出现异常
         */
        data class Exception<T> constructor(
            val exception: Throwable
        ) : Failure<T>() {}
    }
}
复制代码

  利用kotlin密封类的特性(子类数量是有限的,可以通过枚举把所有子类遍历),总共定义5个类型,不过真正使用的其实是SuccessServerErrorException三个。

  • Success指的是网络请求成功
  • ServerError指的是Http协议报文中的状态码不在200-300区间内,通常为404等
  • Exception指的是网络请求过程中发生了异常,例如超时异常,网络断开异常,实体类解析异常等

问:为什么不直接设置成功和失败两种类型,而是要给失败添加两个子类?
答:因为本质上,服务器内部错误并不是发生了异常,而是服务器返回了报文,只是这个报文在协议层面是错误而已,当然你也可以将服务器内部错误转成某个IO异常,我不希望这样做,因为这样容易混淆两者,在某些特殊的场景下会让问题更加难以处理。

  接下来,我们修改接口,将方法返回值改为NetworkResult

interface FriendService {

    @POST("/friend/list")
    suspend fun requestFriend(
        @Body friendRequestParam: FriendRequestParam
    ): NetworkResult<Friends>
    
    data class FriendRequestParam(
        private val page: String,
    )
}
复制代码

  不幸的是,如果你直接这样做,Retrofit将会直接崩溃,因为道理也很简单,Retrofit只知道如何处理返回Call,而不知道如何返回我们自己定义的NetworkResult。

test4.png

三:重写CallAdapter

  所谓的CallAdapter,就是对原来的Call进行适配,对方法的返回值进行扩展,废话不多说,直接看代码。

internal class ApexResponseCallDelegate<T>(private val proxyCall: Call<T>) :
    Call<NetworkResult<T>> {

    override fun enqueue(callback: Callback<NetworkResult<T>>) =
        proxyCall.enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                //得到响应,将被代理的Call的响应包装成NetworkResult后返回给代理者
                callback.onResponse(
                    this@ApexResponseCallDelegate,
                    Response.success(
                        //将Retrofit中的Response转成NetworkResult
                        response.toNetworkResult()
                    )
                )
            }

            override fun onFailure(call: Call<T>, t: Throwable) {
                //失败了,将被代理的Call的异常包装成NetworkResult后返回给代理者
                callback.onResponse(
                    this@ApexResponseCallDelegate,
                    //将Exception转成NetworkResult
                    Response.success(t.toExceptionResult())
                )
            }

        })

    //下面都是不变的代理方法
    override fun isExecuted(): Boolean = proxyCall.isExecuted

    override fun cancel() = proxyCall.cancel()

    override fun isCanceled(): Boolean = proxyCall.isCanceled

    override fun request(): Request = proxyCall.request()

    override fun timeout(): Timeout = proxyCall.timeout()

    override fun clone(): Call<NetworkResult<T>> =
        ApexResponseCallDelegate(proxyCall.clone())

    override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError()
}

//相关的扩展方法如下:
//将Retrofit的Response转成Success
fun <T> Response<T>.toSuccessResult(): NetworkResult.Success<T> {
    return NetworkResult.Success(this)
}

//将Retrofit的Response转成ServerError
fun <T> Response<T>.toServerErrorResult(): NetworkResult.Failure.ServerError<T> {
    return NetworkResult.Failure.ServerError(this)
}

//将Exception转成NetworkResult.Failure.Exception
fun <T> Throwable.toExceptionResult(): NetworkResult.Failure.Exception<T> {
    return NetworkResult.Failure.Exception(this)
}

//将一个Retrofit的Response转成NetworkResult
fun <T> Response<T>.toNetworkResult(): NetworkResult<T> = (try {
    //http code为成功,即200-300
    if (isSuccessful) {
        toSuccessResult()
    } else if (this.body() == null) {
        //自定义的异常
        MsgException("response body is null").toExceptionResult()
    }
    //http code为失败
    else {
        toServerErrorResult()
    }
} catch (t: Throwable) {
    t.toExceptionResult()
})
复制代码

  代码特别多,有一点需要注意的是,无论被代理的Call的网络请求是成功还是失败,最终返回的都是Response.success。这一点确保了我们定义的方法,无论如何都不会对外抛出异常,无论网络请求成功还是失败,都会当做某种“成功”来处理,你可以理解成“网络请求结束”,原本网络请求失败的结果,已经被包裹在NetworkResult里面了。

  下面是CallAdapter本体,以及附属的工厂类

class ApexCallAdapterFactory @Inject constructor() : CallAdapter.Factory() {

    class ApexCallAdapter constructor(
        private val resultType: Type
    ) : CallAdapter<Type, Call<NetworkResult<Type>>> {

        override fun responseType() = resultType

        override fun adapt(call: Call<Type>): Call<NetworkResult<Type>> =
            ApexResponseCallDelegate(call)

    }

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): ApexCallAdapter? = when (getRawType(returnType)) {
        Call::class.java -> {
            //判断方法返回值
            val callType = getParameterUpperBound(0, returnType as ParameterizedType)
            //只有返回值是NetworkResult的才会使用本Adapter
            when (getRawType(callType)) {
                NetworkResult::class.java -> {
                    val resultType = getParameterUpperBound(0, callType as ParameterizedType)
                    ApexCallAdapter(resultType)
                }
                else -> null
            }
        }
        else -> null
    }


}
复制代码

  代码也挺多的,本质上是定义了一个Adapter以及工厂类,工厂类的get用于判断当前的方法是否适合使用工厂类对应的Adapter(因为Retrofit允许同时存在多个Adapter以应对不同的返回值,因此我们需要判断方法返回值是否是NetworkResult)

  最后的最后,在Retrofit的构造方法中,添加我们自己实现的Adapter工厂类即可,Retrofit就会识别出我们定义的NetworkResult并正确的返回结果啦,记得要把方法改成suspend

Retrofit.Builder()
    .baseUrl(baseUrl)
    .addCallAdapterFactory(callAdapterFactory)
    .client(okHttpClient)
    .build()
    

//请求结果
val result=friendService.requestFriend(FriendService.FriendRequestParam(nextPage))
when(result){
    //判断类型
    is Success->{}
    is Exception->{}

}

复制代码

  每次都要手写when代码着实有些蛋疼,让我们给NetworkResult扩充一些方法,自动帮我们完成判断的逻辑

/**
 * 网络请求成功时操作
 */
inline fun <reified T> NetworkResult<T>.ifSuccess(action: (NetworkResult.Success<T>) -> Unit): NetworkResult<T> {
    if (this is NetworkResult.Success) action(this)
    return this
}

/**
 * 网络请求中,服务器已响应但HTTP协议出现错误时(例如404,502)操作
 */
inline fun <reified T> NetworkResult<T>.ifServerError(action: (NetworkResult.Failure.ServerError<T>) -> Unit): NetworkResult<T> {
    if (this is NetworkResult.Failure.ServerError) action(this)
    return this
}

/**
 * 网络请求中,出现异常时(例如解析JSON异常,超时异常,连接网络失败异常等)操作
 */
inline fun <reified T> NetworkResult<T>.ifException(action: (NetworkResult.Failure.Exception<T>) -> Unit): NetworkResult<T> {
    if (this is NetworkResult.Failure.Exception) action(this)
    return this
}

/**
 * 网络请求失败(包括[ifServerError]和[ifException]两种情况)
 */
inline fun <reified T> NetworkResult<T>.ifFailure(action: (errorMsg: String) -> Unit): NetworkResult<T> {
    ifServerError {
        //errorBodyString()方法在下篇文章中会提到
        action(it.errorBodyString())
    }.ifException {
        //exceptionMessage()方法在下篇文章中会提到
        action(it.exceptionMessage())
    }
    return this
}
复制代码

  接下来使用起来就是这样了(切记在协程作用域中调用本方法哦,因为是suspend方法)

searchRepository.search(SearchService.SearchRequestBody(key)).ifSuccess {
    //成功时
}.ifFailure {
    //失败时
}.ifServerError {
    //服务器内部错误时
}.ifException {
    //发生了异常时
}
复制代码

文章到这里已经结束了,第一篇中我们实现了一套CallAdapter,这套adpter的意义是消除了原来的回调式代码,让回调风格代码“拉平”了,变成了我们熟悉的同步代码,下一篇我将解决几个项目中遇到的常见问题。

猜你喜欢

转载自juejin.im/post/7106453717620097031