*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
本文会讲解Coroutine的优点,以及一步步的从零开始改造 Retrofit+Coroutine,对改造中的关键问题进行讲解,给出详细可运行的示例代码。最后会给出Demo,Demo经过简单修改可以直接运用在自己的实际项目中。
Kotlin coroutines let you convert callback-based code to sequential code. Code written sequentially is typically easier to read, and can even use language features such as exceptions.
Coroutine可以让你把基于callback的代码转换成顺序代码.
使用Coroutine会有以下几个好处:
详细的Coroutine介绍,参考官方教程 Coroutines in Kotlin
如果将Coroutine与Retrofit结合起来,就能将Coroutine的优点用于网络访问代码。
Retrofit 从 2.6.0版本开始支持Coroutine
Retrofit Change Log
我们用Retrofit + Coroutine来写一个API的示例,其中会用到Jetpack的ViewModel,LiveData等组件。
我们来访问有道词典的API,来翻译一个单词。
http://fanyi.youdao.com/translate?doctype=json&i=Hello%20world
此时会返回:
{
"type": "EN2ZH_CN",
"errorCode": 0,
"elapsedTime": 1,
"translateResult": [
[
{
"src": "Hello world",
"tgt": "你好,世界"
}
]
]
}
我们来实现这个API。
NetworkBase.kt
package com.jst.network
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
private const val BASE_URL = "http://fanyi.youdao.com/"
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
将retrofit实例对象直接定义在包下面,这样其他类在用的时候可以直接用"retrofit",不用使用 “类名.retrofit”,十分方便
TranslateApiService.kt
interface TranslateApiService {
@FormUrlEncoded
@POST("translate?doctype=json")
suspend fun translate(@Field("i")i:String):Result
}
data class Result(
val type: String,
val elapsedTime: Int,
val translateResult: List<List<TranslateResult>>
) {
data class TranslateResult(
val src:String,
val tgt:String
)
}
object TranslateApi{
val retrofitService: TranslateApiService by lazy { retrofit.create(TranslateApiService::class.java) }
}
kotlin里可以将多个类定义在一个文件里,因为一个API接口会包含若干个相关的类,所以我们把这些类定义在一个文件里会方便管理,也使得我们的工程里没有那么多碎的文件,看起来会很简洁。
我们之所以会定义一个object TranslateApi,是因为
retrofit.create(TranslateApiService::class.java)
是一个比较重的操作,所以我们将结果放在object里保存,这样下次再用的时候就不会重复调用retrofit.create
by lazy{} 可以实现延迟初始化,当property retrofitService 被第一次访问时执行lazy{} 块里的代码返回一个TranslateApiService实例赋值给retrofitService,下次再访问时会复用之前返回的TranslateApiService实例,详见kotlin里的Delegated Properties
MainViewModel
class MainViewModel : ViewModel() {
private val _translateResult: MutableLiveData<String> = MutableLiveData()
val translateResult: LiveData<String>
get() = _translateResult
fun translate(word: String) {
viewModelScope.launch {
val result = TranslateApi.retrofitService.translate(word)
_translateResult.value = result.translateResult[0][0].tgt
}
}
}
我们可以看网络访问的代码,就一行,使用Coroutine以后这里没有callback,代码是顺序代码,就像你想要表达的逻辑那样顺序写下来。这代码可读性是不是好到没朋友。
MainFragment:
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private val viewModel: MainViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val word = "Hello world"
textview.text = "正在翻译……"
viewModel.translateResult.observe(viewLifecycleOwner){
textview.text = "原词: $word \n翻译: $it"
}
viewModel.translate(word)
}
}
使用ViewModel+LiveData,我们的代码逻辑也变得很清晰。
我们的示例代码已经搭建完了,运行一下
能够正确显示翻译结果,说明我们的网络请求从请求发出,到json数据反序列化成对象这整套流程是正常的。
代码逻辑清晰,测试结果正常,是不是一切都ok了呢?
显然不是的,我们少考虑了一种情况,如果没有网络,或者服务器异常,那么
val result = TranslateApi.retrofitService.translate(word)
这里的result会返回什么呢?
我们将网络断开,再测试一下:
发现程序崩溃了!
我们肯定不能在异常情况下直接让程序崩溃,所以我们要处理异常情况。
之所以会崩溃是因为
val result = TranslateApi.retrofitService.translate(word)
在异常情况下会throw Exception,而我们的程序没有处理Exception,所以程序崩溃了。
很容易我们就想到,可以try catch Exception
修改后的 MainViewModel:
class MainViewModel : ViewModel() {
private val _translateResult: MutableLiveData<String> = MutableLiveData()
val translateResult: LiveData<String>
get() = _translateResult
fun translate(word: String) {
viewModelScope.launch {
try {
val result = TranslateApi.retrofitService.translate(word)
_translateResult.value = result.translateResult[0][0].tgt
} catch (e: Exception) {
_translateResult.value = e.message
}
}
}
}
再次运行,发现程序不崩溃了。
这样是不是就ok了呢?
崩溃确实是不崩溃了,但是考虑我们在开发过程中的实际需求,这种方案存在几个问题:
比如,开发人员很容易忘了try catch,而且这种情况下也没有编译提示。他测试的时候网络很好,所以一切正常,但是到用户那一旦网络不好就崩溃了
我们通常的网络框架在异常的时候,需要提供errorCode 和errorMsg,目前这种try catch的方案无法满足我们的需求。
基于上面两个原因,我们需要重构异常处理。
我们不想依赖于try catch,但是又想处理网络请求过程中的异常情况,而且要增加errorCode和errorMsg异常信息,我们如何来实现呢?
我们可以自定义Retrofit的CallAdapter来满足我们的需求。
CallAdapter是由CallAdapterFactory生成的,我们先来看一下CallAdapterFactory的定义
CallAdapter.Factory:
/**
* Creates {@link CallAdapter} instances based on the return type of {@linkplain
* Retrofit#create(Class) the service interface} methods.
*/
abstract class Factory {
/**
* Returns a call adapter for interface methods that return {@code returnType}, or null if it
* cannot be handled by this factory.
*/
public abstract @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit);
/**
* Extract the upper bound of the generic parameter at {@code index} from {@code type}. For
* example, index 1 of {@code Map} returns {@code Runnable}.
*/
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
/**
* Extract the raw class type from {@code type}. For example, the type representing {@code
* List extends Runnable>} returns {@code List.class}.
*/
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
源码注释已经写的很清楚了,这里面每个方法的注释都要读,后面写的时候都会用到。
总结:
Factory的作用就是根据你定义的接口(上例中的TranslateApiService)的返回值类型来判断是否是该CallAdapter需要处理的返回值类型,如果是,则返回一个处理该返回值类型的CallAdapter,如果不是,则返回null
CallAdapter:
/**
* Adapts a {@link Call} with response type {@code R} into the type of {@code T}. Instances are
* created by {@linkplain Factory a factory} which is {@linkplain
* Retrofit.Builder#addCallAdapterFactory(Factory) installed} into the {@link Retrofit} instance.
*/
public interface CallAdapter<R, T> {
/**
* Returns the value type that this adapter uses when converting the HTTP response body to a Java
* object. For example, the response type for {@code Call} is {@code Repo}. This type is
* used to prepare the {@code call} passed to {@code #adapt}.
*
* Note: This is typically not the same type as the {@code returnType} provided to this call
* adapter's factory.
*/
Type responseType();
/**
* Returns an instance of {@code T} which delegates to {@code call}.
*
* For example, given an instance for a hypothetical utility, {@code Async}, this instance
* would return a new {@code Async} which invoked {@code call} when run.
*
*
* @Override
* public <R> Async<R> adapt(final Call<R> call) {
* return Async.create(new Callable<Response<R>>() {
* @Override
* public Response<R> call() throws Exception {
* return call.execute();
* }
* });
* }
*
*/
T adapt(Call<R> call);
}
这里面的每个注释也都要看,这里的方法后面写的时候也都会用到。
总结:
T adapt(Call
call);
CallAdapter顾名思义就是Call的Adapter,Call指的是retrofit里的Call对象retrofit2.Call
Call:
/**
* An invocation of a Retrofit method that sends a request to a webserver and returns a response.
* Each call yields its own HTTP request and response pair. Use {@link #clone} to make multiple
* calls with the same parameters to the same webserver; this may be used to implement polling or to
* retry a failed call.
*
* Calls may be executed synchronously with {@link #execute}, or asynchronously with {@link
* #enqueue}. In either case the call can be canceled at any time with {@link #cancel}. A call that
* is busy writing its request or reading its response may receive a {@link IOException}; this is
* working as designed.
*
* @param Successful response body type.
*/
public interface Call<T> extends Cloneable {
/**
* Synchronously send the request and return its response.
*
* @throws IOException if a problem occurred talking to the server.
* @throws RuntimeException (and subclasses) if an unexpected error occurs creating the request or
* decoding the response.
*/
Response<T> execute() throws IOException;
/**
* Asynchronously send the request and notify {@code callback} of its response or if an error
* occurred talking to the server, creating the request, or processing the response.
*/
void enqueue(Callback<T> callback);
/**
* Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain
* #enqueue(Callback) enqueued}. It is an error to execute or enqueue a call more than once.
*/
boolean isExecuted();
/**
* Cancel this call. An attempt will be made to cancel in-flight calls, and if the call has not
* yet been executed it never will be.
*/
void cancel();
/** True if {@link #cancel()} was called. */
boolean isCanceled();
/**
* Create a new, identical call to this one which can be enqueued or executed even if this call
* has already been.
*/
Call<T> clone();
/** The original HTTP request. */
Request request();
/**
* Returns a timeout that spans the entire call: resolving DNS, connecting, writing the request
* body, server processing, and reading the response body. If the call requires redirects or
* retries all must complete within one timeout period.
*/
Timeout timeout();
}
这个Call对象可以完成同步或异步的网络请求。
Retrofit把这样的一个Call对象传递给adapt方法,然后不同的CallAdapter可以将这个Call对象包装成你需要返回给用户使用的T对象,T对象的内部是使用Call对象的网络请求的能力。
比如像Rxjava,我们需要让用户在定义网络接口的时候直接返回一个Observable对象
interface MyService {
@GET("/user")
Observable<User> getUser();
}
那么我们就可以定义一个RxJavaCallAdapter来将Call对象包装成Observable对象。当然对于RxJava的这种情况,官方已经提供了CallAdapter。
针对我们目前的需求,我们需要在网络请求成功的时候返回数据Bean对象,在网络请求失败的时候返回一个包含了errorCode和errorMsg的对象,Kotlin的sealed class比较适合我们的场景。
ApiResult:
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>()
}
使用ApiResult后的TranslateApiService:
interface TranslateApiService {
@FormUrlEncoded
@POST("translate?doctype=json")
suspend fun translate(@Field("i")i:String):ApiResult<Result>
}
所以接下来我们的CallAdapter的任务就是将Call对象包装成ApiResult对象。
但这里面还有一个很关键的问题。
call.enqueue(new Callback<T>() {
//请求成功时候的回调
@Override
public void onResponse(Call<T> call, Response<T> response) {
// 将 response.body() 作为suspend方法成功时的返回值
}
//请求失败时候的回调
@Override
public void onFailure(Call<Translation> call, Throwable throwable) {
// 将 throwable 作为suspend方法失败时的异常抛出
}
});
所以我们对于suspend方法自定义CallAdapter时要将它作为非suspend方法来看待。
我们定义的
interface TranslateApiService {
@FormUrlEncoded
@POST("translate?doctype=json")
suspend fun translate(@Field("i")i:String):ApiResult<Result>
}
相当于定义了
interface TranslateApiService {
@FormUrlEncoded
@POST("translate?doctype=json")
fun translate(@Field("i")i:String):Call<ApiResult<Result>>
}
所以我们的CallAdapter的作用就是处理返回值类型为Call
fun adapt(call: Call<T>): Call<ApiResult<T>>
下面看一下具体实现
ApiResultCallAdapter.kt:
package com.jst.network.calladapter
import com.jst.network.ApiError
import com.jst.network.ApiResult
import com.jst.network.exception.ApiException
import okhttp3.Request
import okio.Timeout
import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class ApiResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
/*凡是检测不通过的,直接抛异常,提示使用者返回值类型格式不对
因为ApiResultCallAdapterFactory是使用者显式设置使用的*/
//以下是检查是否是 Call> 类型的returnType
//检查returnType是否是Call类型的
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
//取出Call 里的T,检查是否是ApiResult
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" }
//取出ApiResult中的T 也就是API返回数据对应的数据类型
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>> {
/**
* 该方法会被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对象,网络请求的成功失败会回调不同的方法
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))
}
})
}
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()
}
}
在关键的地方都做了注释,看注释逻辑还是比较清晰的。
看完代码后你会发现这里面还有一个未实现的部分,就是处理业务异常的Interceptor:
业务异常指的是服务器正常返回了数据,但是errorCode不是成功,而是代表了某种业务错误,此时有可能不会返回你定义的业务成功的数据Bean。
以翻译API为例
业务成功时返回的json格式:
{
"type": "EN2ZH_CN",
"errorCode": 0,
"elapsedTime": 1,
"translateResult": [
[
{
"src": "Hello world",
"tgt": "你好,世界"
}
]
]
}
业务失败时有可能返回的json格式:
{
"errorCode": 50,
"errorMsg": "xxx",
}
我们可以定义一个Interceptor来处理这种情况:
在errorCode不是0的情况下,抛出异常,异常信息里包含errorCode和errorMsg,如果你在Interceptor里抛出了异常,OkHttp会终止本次请求,回调onFailure方法。
BusinessErrorInterceptor:
/**
* 业务错误 Interceptor
* 对于request: 无
* 对于response:负责解析业务错误(在http status 成功的前提下)
*/
class BusinessErrorInterceptor :Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
var response = chain.proceed(chain.request())
//http status不是成功的情况下,我们不处理
if (!response.isSuccessful){
return response
}
//因为response.body().string() 只能调用一次,所以这里读取responseBody不使用response.body().string(),原因:https://juejin.im/post/6844903545628524551
//以下读取resultString的代码节选自
//https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/src/main/kotlin/okhttp3/logging/HttpLoggingInterceptor.kt
val responseBody = response.body()!!
val source = responseBody.source()
source.request(Long.MAX_VALUE) // Buffer the entire body.
var buffer = source.buffer
val contentType = responseBody.contentType()
val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
val resultString = buffer.clone().readString(charset)
val jsonObject = JSONObject(resultString)
if (!jsonObject.has("errorCode")) {
return response
}
val errorCode = jsonObject.optInt("errorCode")
//对于业务成功的情况不做处理
if (errorCode == 0) {
return response
}
//我们的示例里服务器没有返回errorMsg,一般实际应用中服务器都会有errorMsg
throw ApiException(errorCode, "some error msg")
}
}
这里需要注意response.body().string() 只能调用一次的问题,所以这里我们不能调用response.body().string(),具体原因:
https://juejin.im/post/6844903545628524551
我们读取resultString的代码节选自okhttp官方的HttpLoggingInterceptor.kt
ApiException:
class ApiException(val errorCode:Int,val errorMsg:String): IOException()
这里需要注意的是我们自定义的ApiException必须继承自IOException,因为只有IOException才会被OkHttp处理,然后回调到onFailure方法,其他类型的异常是直接就崩溃了。
回到前面的ApiResultCallAdapter.kt,我们可以看到对ApiException的处理
ApiResultCallAdapter.kt:
/**
* 在网络请求中发生了异常,会回调该方法
*
* 对于网络请求成功,但是业务失败的情况,我们也会在对应的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))
}
最后我们添加ApiResultCallAdapter和BusinessErrorInterceptor
NetworkBase.kt:
private const val BASE_URL = "http://fanyi.youdao.com/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(BusinessErrorInterceptor())
.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(ApiResultCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()
此时调用接口时的代码就变成了:
MainViewModel:
class MainViewModel : ViewModel() {
private val _translateResult: MutableLiveData<String> = MutableLiveData()
val translateResult: LiveData<String>
get() = _translateResult
fun translate(word: String) {
viewModelScope.launch {
when (val result = TranslateApi.retrofitService.translate(word)) {
is ApiResult.Success -> {
_translateResult.value = result.data.translateResult1[0][0].tgt
}
is ApiResult.Failure -> {
_translateResult.value = "errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}"
}
}
}
}
}
没有callback,我们的代码还是顺序代码,同时也保证了在成功时拿到数据对象,在失败时拿到errorCode和errorMsg。
我们来验证一下失败时的情况:
翻译API在单词过长的情况下,会返回错误。
比如我们翻译"IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII"
此时返回的json:
{
"type": "UNSUPPORTED",
"errorCode": 40,
"elapsedTime": 0,
"translateResult": [
[
{
"src": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII",
"tgt": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII"
}
]
]
}
此时我们的程序运行的情况:
正确的解析出了errorCode和errorMsg信息
https://github.com/ShuangtaoJia/RetrofitWithCoroutineDemo
Demo中的代码将参数名按照项目的实际情况简单修改,就可以放在实际项目中使用。
Retrofit+Coroutine 会是以后的主流形式,因为有官方的支持再加上自身的诸多优点,会逐步的取代RxJava等其他技术,大家可以尽早体验使用。
有问题欢迎留言。