作者:FredYe
近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:
方案一
代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:
- 服务的定义
1
2
3
4
5
6interface ApiService { @GET("users") suspend fun getUsers(): List<User> }
- Retrofit Builder
1
2
3
4
5
6
7
8
9
10
11
12
13
14object 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
2
3
4
5class ApiHelper(private val apiService: ApiService) { suspend fun getUsers() = apiService.getUsers() }
1
2
3
4
5class MainRepository(private val apiHelper: ApiHelper) { suspend fun getUsers() = apiHelper.getUsers() }
- 在ViewModel中获取网络数据
1
2
3
4
5
6
7
8
9
10
11
12class 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了。相关的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30open 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里面这样写
1
2
3
4
5
6
7
8
9
10
11class 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错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60class 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)
- 方便的异常处理
调整后的方案
以下代码的相关依赖库版本
1
2
3
4
5
6
7implementation '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
1
2
3
4
5
6
7
8object 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, 这些数据会向上抛给调用方。
1
2
3
4
5
6sealed 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
1
2
3
4
5interface WanAndroidApi { @GET("/banner/json") suspend fun getBanner(): ApiResult<ApiResponse<List<Banner>>> }
- 定义一个
ApiCallAdapterFactory.kt
在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80class 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
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16object 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中使用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13viewModelScope.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
1
2
3
4
5interface WanAndroidApi { @GET("/banner/json") fun getBanner2(): Call<ApiResponse<List<Banner>>> }
需要注意此处的getBanner2()
方法前面没有suspend
关键字,返回的是一个Call
类型的对象,这个很重要。
- 定义一个
CallWait.kt
文件, 为Call
类添加扩展方法awaitResult
, 该方法内部有部份逻辑和上面的CallAdapter
中的实现类似。CallWait.kt
文件也是借鉴了这段代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31suspend 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15object 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
方法
1
2
3
4
5
6
7
8
9
10
11
12
13viewModelScope.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
, 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18try { 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去实现一套
- 定义服务
1
2
3
4
5public interface WanAndroidApiJava { @GET("/banner/json") public Call<NetResult<List<Banner>>> getBanner(); }
ApiException
中去封装错误信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public 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
封装服务器的响应
1
2
3
4
5
6
7public class NetResult<T> { private T data; private int code; private String errorMsg; ...//省略get/set }
- 自定义一个Callback去解析数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public 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
2
3
4
5
6
7
8
9
10
11
12
13
14api.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内容请搜索靠谱客的其他文章。
发表评论 取消回复