前言:由于框架本身也在不断地迭代,因此文章中的部分代码可能存在更新或者过时,如果你想阅读源码或者查看代码的在项目中的实际使用方法,可以查看笔者目前在维护的compose项目:Spacecraft: 《Spacecraft - 我的安卓技术实践平台》-查看代码请进入develop分支 (gitee.com)
Retrofit框架之美
在众多安卓网络请求框架中,Retrofit无疑是最耀眼的那个:基于okhttp实现,迁移成本低、API设计简洁易用、注解化配置高度解耦、支持多种解析器、支持rxjava和协程。。。
然而实际项目中,Retrofit有诸多不便,例如:返回值Call容易陷入嵌套地域、网上大多数CallAdapter方案需要手动try-catch、缺乏全局逻辑等。不过好消息是,得益于Retrofit强大的定制功能,我们可以逐一解决上诉的痛点,让Retrofit从一个花瓶变成一个具有实战意义的工具。
我们的目标是让Retrofit拥有以下功能和特性:
- 在协程下执行网络请求,让异步请求代码变成近似同步代码,消除嵌套地狱
- 网络API的注解自定义和逻辑自定义
- 增加可配置的全局结果转换器,例如返回的成功报文中,含有code为非0的结果,需要转换成错误的报文
- 网络请求方法不用手动try-catch异常,即方法不向上抛出,方法返回值包含网络异常
阅读本系列需要以下知识点:
- kotlin
- Hilt(最好了解)
- Retrofit(基础或者使用过即可)
- okhttp(了解拦截器的使用)
一:分析问题
默认情况下,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个类型,不过真正使用的其实是Success、ServerError、Exception三个。
- 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。