概述
作者:FredYe
近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:
方案一
代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:
- 服务的定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
- 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)
}
- 一些中间层
class ApiHelper(private val apiService: ApiService) {
suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers()
}
- 在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!"))
}
}
}
这段代码能够与服务端通信,满足基本的要求,并且也有异常的处理机制。但存在以下问题:
- 对异常的处理粒度过大。如果需要对不同的异常进行差异化的处理,就会比较麻烦。
- 在每一个调用的地方都需要进行try…catch操作。
- 不支持从reponse中获取响应头部, http code 信息。但其实很多APP通常也没有要求做这些处理,如果没有拿到数据,给一个通用的提示就完。所以这种方案在某些情况下是可以直接使用的。
方案二
从Github上找了一个Demo, 链接在这里 和方案一相比,作者在的BaseRepository里面,对接口的调用统一进行了try…catch的处理,这样对于调用方,就不用每一个都添加try…catch了。相关的代码如下:
open class BaseRepository {
suspend fun <T : Any> apiCall(call: suspend () -> WanResponse<T>): WanResponse<T> {
return call.invoke()
}
suspend fun <T : Any> safeApiCall(call: suspend () -> Result<T>, errorMessage: String): Result<T> {
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 <T : Any> executeResponse(response: WanResponse<T>, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null): Result<T> {
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<List<Banner>> {
return safeApiCall(call = {requestBanners()},errorMessage = "")
}
private suspend fun requestBanners(): Result<List<Banner>> =
executeResponse(WanRetrofitClient.service.getBanner())
}
方案三
在网上看到这个博客, 作者利用一个CallAdapter进行转换,将http错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
/**
* 该方法会被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<ApiResult<T>>) {
//delegate 是用来做实际的网络请求的Call<T>对象,网络请求的成功失败会回调不同的方法
delegate.enqueue(object : Callback<T> {
/**
* 网络请求成功返回,会回调该方法(无论status code是不是200)
*/
override fun onResponse(call: Call<T>, response: Response<T>) {
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>, 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,还是那句话,毕竟这个需求不常见,可以忽略。
总结一下,当前网上的这些方案可能有的局限:
- 如果服务器出错了,不能拿到具体的错误信息。比如,如果服务器返回401, 403,这些方案中的网络层不能将这些信息传递出去。
- 如果服务端通过header传递数据给前端,这些方案是不满足需求的。
针对上面的两个问题,我们来考虑如何完善框架的实现。
调整思路
我们期望一个网络请求方案能满足如下目标:
- 与服务器之间的正常通信
- 能拿到响应体中的header数据
- 能拿到服务器的出错信息(http code,message)
- 方便的异常处理
调整后的方案
以下代码的相关依赖库版本
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"
- 约定常见的错误类型
我们期望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)
- 返回数据的定义
ApiResult.kt
用来承载返回的数据,成功时返回正常的业务数据,出错时组装errorCode, errorMsg, 这些数据会向上抛给调用方。
sealed class ApiResult<out T>() {
data class Success<out T>(val data: T):ApiResult<T>()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult<Nothing>()
}
data class ApiResponse<out T>(var errorCode: Int, var errorMsg: String, val data: T)
方案一
该方案支持获取HTTP Code,并返回给调用方, 不支持从HTTP Response中提取header的数据。
- 服务的定义
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult<ApiResponse<List<Banner>>>
}
- 定义一个
ApiCallAdapterFactory.kt
在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。
class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, 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<Any>(dataType)
}
}
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
override fun enqueue(callback: Callback<ApiResult<T>>) {
delegate.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
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>, 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<ApiResult<T>> = ApiResultCall(delegate.clone())
override fun execute(): Response<ApiResult<T>> {
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()
}
}
- 在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 <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
- 在ViewModel中使用如下:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
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中获取数据。
- 服务的定义
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call<ApiResponse<List<Banner>>>
}
需要注意此处的getBanner2()
方法前面没有suspend
关键字,返回的是一个Call
类型的对象,这个很重要。
- 定义一个
CallWait.kt
文件, 为Call
类添加扩展方法awaitResult
, 该方法内部有部份逻辑和上面的CallAdapter
中的实现类似。CallWait.kt
文件也是借鉴了这段代码
suspend fun <T : Any> Call<T>.awaitResult(): ApiResult<T> {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>?, response: Response<T>) {
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>, 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)
}
}
})
}
}
- 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 <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
- 在
ViewModel
中使用, 和方法一基本一致,只是这里需要调用一下awaitResult
方法
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")
}
}
}
- 如果我们想从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<List<Banner>>) {
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<NetResult<List<Banner>>> 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<T> {
private T data;
private int code;
private String errorMsg;
...//省略get/set
}
- 自定义一个Callback去解析数据
public abstract class RetrofitCallbackEx<T> implements Callback<NetResult<T>> {
@Override
public void onResponse(Call<NetResult<T>> call, Response<NetResult<T>> response) {
//如果返回成功
if (response.isSuccessful()) {
NetResult<T> 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<NetResult<T>> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}
protected abstract void onSuccess(T t);
protected abstract void onFail(ApiException e);
}
- 使用
api.getBanner().enqueue(new RetrofitCallbackEx<List<Banner>>() {
@Override
protected void onSuccess(List<Banner> 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());
}
});
其它
- 在实际项目中,可能经常会碰到需要对HTTP Code进行全局处理的,比如当服务器返回401的时候,引导用户去登录页,这种全局的拦截直接放到interceptor 里面去做就好了。
- 架构的方案是为了满足业务的需求,这里也只是针对自己碰到的业务场景来进行梳理调研。当然实际项目中通常会有更多的要求,比如环境的切换导致域名的不同,网络请求的通用配置,业务异常的上报等等,一个完整的网络请求方案需要再添加更多的功能。
- Kotlin语言非常的灵活,扩展函数的使用能使代码非常的简洁。Kotlin在我们项目中用的不多, 不是非常精通,协程 + Retrofit应该会有更优雅的写法,欢迎交流。
最后
以上就是无私美女为你收集整理的kotlin 协程 + Retrofit 搭建网络请求方案对比的全部内容,希望文章能够帮你解决kotlin 协程 + Retrofit 搭建网络请求方案对比所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复