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

10,087 阅读9分钟

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

方案一

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

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

}
  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 <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,还是那句话,毕竟这个需求不常见,可以忽略。

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

  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<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的数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
    @GET("/banner/json")
    suspend fun getBanner(): ApiResult<ApiResponse<List<Banner>>>
}
  1. 定义一个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()
    }
}
  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 <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
    inline fun <reified T> create(): T = create(T::class.java)
}
  1. 在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中获取数据。

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

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

  1. 定义一个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)
                }
            }
        })
    }
}
  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 <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
    inline fun <reified T> 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<List<Banner>>
           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<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);

}
  1. 使用
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());
    }
});

其它

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

参考