注册

kotlin 协程 + Retrofit 搭建网络请求方案对比

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:

方案一

代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:

  1. 服务的定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List

}
  1. Retrofit Builder
object RetrofitBuilder {

private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"

private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}

val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
  1. 一些中间层
class ApiHelper(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {

suspend fun getUsers() = apiHelper.getUsers()
}
  1. 在ViewModel中获取网络数据
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
}
}

这段代码能够与服务端通信,满足基本的要求,并且也有异常的处理机制。但存在以下问题:

  1. 对异常的处理粒度过大。如果需要对不同的异常进行差异化的处理,就会比较麻烦。
  2. 在每一个调用的地方都需要进行try...catch操作。
  3. 不支持从reponse中获取响应头部, http code 信息。但其实很多APP通常也没有要求做这些处理,如果没有拿到数据,给一个通用的提示就完。所以这种方案在某些情况下是可以直接使用的。

方案二

从Github上找了一个Demo, 链接在这里 和方案一相比,作者在的BaseRepository里面,对接口的调用统一进行了try...catch的处理,这样对于调用方,就不用每一个都添加try...catch了。相关的代码如下:

open class BaseRepository {

suspend fun apiCall(call: suspend () -> WanResponse): WanResponse {
return call.invoke()
}

suspend fun safeApiCall(call: suspend () -> Result, errorMessage: String): Result {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}

suspend fun executeResponse(response: WanResponse, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null)
: Result {
return coroutineScope {
if (response.errorCode == -1) {
errorBlock?.let { it() }
Result.Error(IOException(response.errorMsg))
} else {
successBlock?.let { it() }
Result.Success(response.data)
}
}
}

}

在Repository里面这样写

class HomeRepository : BaseRepository() {

suspend fun getBanners(): Result> {
return safeApiCall(call = {requestBanners()},errorMessage = "")
}

private suspend fun requestBanners(): Result> =
executeResponse(WanRetrofitClient.service.getBanner())

}

方案三

在网上看到这个博客, 作者利用一个CallAdapter进行转换,将http错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:

class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {
/**
* 该方法会被Retrofit处理suspend方法的代码调用,并传进来一个callback,如果你回调了callback.onResponse,那么suspend方法就会成功返回
* 如果你回调了callback.onFailure那么suspend方法就会抛异常
*
* 所以我们这里的实现是永远回调callback.onResponse,只不过在请求成功的时候返回的是ApiResult.Success对象,
* 在失败的时候返回的是ApiResult.Failure对象,这样外面在调用suspend方法的时候就不会抛异常,一定会返回ApiResult.Success 或 ApiResult.Failure
*/

override fun enqueue(callback: Callback>) {
//delegate 是用来做实际的网络请求的Call对象,网络请求的成功失败会回调不同的方法
delegate.enqueue(object : Callback {

/**
* 网络请求成功返回,会回调该方法(无论status code是不是200)
*/

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {//http status 是200+
//这里担心response.body()可能会为null(还没有测到过这种情况),所以做了一下这种情况的处理,
// 处理了这种情况后还有一个好处是我们就能保证我们传给ApiResult.Success的对象就不是null,这样外面用的时候就不用判空了
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {//http status错误
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

/**
* 在网络请求中发生了异常,会回调该方法
*
* 对于网络请求成功,但是业务失败的情况,我们也会在对应的Interceptor中抛出异常,这种情况也会回调该方法
*/

override fun onFailure(call: Call, t: Throwable) {
val failureApiResult = if (t is ApiException) {//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}

callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

})
}
...
}
,>

作者有提供一个Demo, 如果想拿来用,需要自己再新增一个返回数据的包装类。该方案的缺点是不能获取响应体中的header,还是那句话,毕竟这个需求不常见,可以忽略。

总结一下,当前网上的这些方案可能有的局限:

  1. 如果服务器出错了,不能拿到具体的错误信息。比如,如果服务器返回401, 403,这些方案中的网络层不能将这些信息传递出去。
  2. 如果服务端通过header传递数据给前端,这些方案是不满足需求的。

针对上面的两个问题,我们来考虑如何完善框架的实现。

调整思路

我们期望一个网络请求方案能满足如下目标:

  1. 与服务器之间的正常通信
  2. 能拿到响应体中的header数据
  3. 能拿到服务器的出错信息(http code,message)
  4. 方便的异常处理

调整后的方案

以下代码的相关依赖库版本

implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
  1. 约定常见的错误类型

我们期望ApiException中也能够返回HTTP Code, 为此约定,错误信息的code从20000开始,这样就不会和HTTP的Code有冲突了。

  • ApiError
object ApiError {
var unknownError = Error(20000, "unKnown error")
var netError = Error(20001, "net error")
var emptyData = Error(20002, "empty data")
}

data class Error(var errorCode: Int, var errorMsg: String)
  1. 返回数据的定义ApiResult.kt

用来承载返回的数据,成功时返回正常的业务数据,出错时组装errorCode, errorMsg, 这些数据会向上抛给调用方。

sealed class ApiResult() {
data class Success(val data: T):ApiResult()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult()
}
data class ApiResponse(var errorCode: Int, var errorMsg: String, val data: T)

方案一

该方案支持获取HTTP Code,并返回给调用方, 不支持从HTTP Response中提取header的数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult>>
}
  1. 定义一个ApiCallAdapterFactory.kt

在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。

class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {=
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }

val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) { "$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter(dataType)
}
}
class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {

override fun enqueue(callback: Callback>) {
delegate.enqueue(object : Callback {

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

override fun onFailure(call: Call, t: Throwable) {
//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}

override fun clone(): Call> = ApiResultCall(delegate.clone())

override fun execute(): Response> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")
}


override fun isExecuted(): Boolean {
return delegate.isExecuted
}

override fun cancel() {
delegate.cancel()
}

override fun isCanceled(): Boolean {
return delegate.isCanceled
}

override fun request(): Request {
return delegate.request()
}

override fun timeout(): Timeout {
return delegate.timeout()
}
}
,>
  1. 在Retrofit 初始化时指定CallAdapterFactory, 定义文件ApiServiceCreator.kt 如下:
object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. 在ViewModel中使用如下:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}

方案二

该方案在方案一的基础之上,支持从HTTP Response Header中获取数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call>>
}

需要注意此处的getBanner2()方法前面没有suspend关键字,返回的是一个Call类型的对象,这个很重要。

  1. 定义一个CallWait.kt文件, 为Call类添加扩展方法awaitResult, 该方法内部有部份逻辑和上面的CallAdapter中的实现类似。CallWait.kt文件也是借鉴了这段代码
suspend fun  Call.awaitResult(): ApiResult {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call?, response: Response) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!)
}
} else {
ApiResult.Failure(response.code(), response.message())
}
})
}

override fun onFailure(call: Call, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
  1. Retrofit的初始化

和方案一不一样,在Retrofit 初始化时不需要指定CallAdapterFactory, 定义文件ApiServiceCreator.kt

object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. ViewModel中使用, 和方法一基本一致,只是这里需要调用一下awaitResult方法
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}
  1. 如果我们想从reponse的header里面拿数据, 可以使用Retrofit提供的扩展函数awaitResponse, 如下:
try {
val result = api.getBanner2().awaitResponse()
//拿HTTP Header中的数据
Log.i("API Response", "-----header---->Server:" + result.headers().get("Server"))

if (result.isSuccessful) {
var data = result.body();
if (data != null && data is ApiResponse>) {
Log.i("API Response", "--------->data:" + data.data.size)
}
} else {
//拿HTTP Code
Log.i("API Response","errorCode: ${result.code()}")
}
} catch (e: Exception) {
Log.i("API Response","exception: ${e.message}");
}

方案三

如果我们用Java去实现一套

  • 定义服务
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call>> getBanner();
}
  • ApiException中去封装错误信息
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;

public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}

public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}

public String getErrorMessage() {
return this.errorMessage;
}

public int getErrorCode() {
return this.errorCode;
}

interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}

public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, "数据解析出错");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, "服务器响应出错");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, "网络连接出错");
}
  • NetResult封装服务器的响应
public class NetResult {
private T data;
private int code;
private String errorMsg;
...//省略get/set
}
  • 自定义一个Callback去解析数据
public abstract class RetrofitCallbackEx implements Callback> {

@Override
public void onResponse(Call> call, Response> response) {
//如果返回成功
if (response.isSuccessful()) {
NetResult data = response.body();
//返回正确, 和后端约定,返回的数据中code == 0 代表业务成功
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
//数据解析出错
onFail(ApiException.PARSE_ERROR);
}
} else {
onFail(ApiException.SERVER_ERROR);
}
} else {
//服务器请求出错
Log.i("API Response", "code:" + response.code() + " message:" + response.message());
onFail(ApiException.SERVER_ERROR);
}
}

@Override
public void onFailure(Call> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}

protected abstract void onSuccess(T t);

protected abstract void onFail(ApiException e);

}
  1. 使用
api.getBanner().enqueue(new RetrofitCallbackEx>() {
@Override
protected void onSuccess(List banners) {
if (banners != null) {
Log.i("API Response", "data size:" + banners.size());
}
}

@Override
protected void onFail(ApiException e) {
Log.i("API Response", "exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: " + e.getMessage());
}
});

其它

  1. 在实际项目中,可能经常会碰到需要对HTTP Code进行全局处理的,比如当服务器返回401的时候,引导用户去登录页,这种全局的拦截直接放到interceptor 里面去做就好了。
  2. 架构的方案是为了满足业务的需求,这里也只是针对自己碰到的业务场景来进行梳理调研。当然实际项目中通常会有更多的要求,比如环境的切换导致域名的不同,网络请求的通用配置,业务异常的上报等等,一个完整的网络请求方案需要再添加更多的功能。
  3. Kotlin语言非常的灵活,扩展函数的使用能使代码非常的简洁。Kotlin在我们项目中用的不多, 不是非常精通,协程 + Retrofit应该会有更优雅的写法,欢迎交流。


作者:FredYe
链接:https://juejin.cn/post/7064123524587192356
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册