在Retrofit第二章对kotlin协程的支持中,我们了解了Retrofit是如何支持kotlin协程的。写法如下
interface WanAndroidService {
@GET("banner/json")
//更改方法,加上suspend关键字,并且将Call<Banner>直接改为Banner
suspend fun banner(): Banner
}
//进行Retrofit初始化
val retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service: WanAndroidService = retrofit.create(WanAndroidService::class.java)
lifecycleScope.launch{
//这里直接调用banner方法,不用调用execute(),并且运行在子线程
val banner = service.banner()
//主线程运行
tvResult.setText(banner.toString())
}
问题所在
这样的代码在正常情况下是可以运行的,但是在业务开发中,这样的代码是有问题的,因为我们忽略了异常情况的处理。
从Retrofit源码中可以看出,当我们定义的Banner不为空,但是返回结果为空的时候就会抛出异常,如果网络请求失败,也会抛出异常,再比如我将玩Android的baseUrl从wanandroid.com改成wanandroid.com1 就会抛出一个UnknownHostException。
那怎么处理这种异常情况呢?
常规处理
针对这种抛出异常的情况,其实很好解决,try catch即可
lifecycleScope.launch{
try {
val banner = service.banner()
tvResult.setText(result.body().toString())
} catch (e: Exception) {
e.printStackTrace()
}
}
不过,这样的代码,看起来不优雅,而且更关键的是,我忘了写怎么办?又不会提示报错,编译也通过了。这是一个优化点
优化方案
方案1:官方方案
lifecycleScope.launch {
kotlin.runCatching {
service.banner()
}.onSuccess {
//成功执行
Log.i(TAG, "onCreate: " + it)
}.onFailure {
//异常信息处理
}.getOrNull()?.let {
//获取数据为非空
}
}
通过kotlin提供的runCatching函数,可以比较方便地实现异常处理,而且是流式处理,一般来说我们只需要关注onSuccess即可,也可以调用getOrNull()来判断结果是否为空。
这种方案看起来还不错,就是感觉啰嗦了一点,没有统一的异常处理,而且如果有业务层面的errorCode == 0的判断,还是要自己处理。
方案2:Response可空
interface WanAndroidService {
@GET("banner/json")
//改为可空
suspend fun banner(): Banner?
}
lifecycleScope.launch{
val banner = service.banner()
//判空
if(banner != null) {
tvResult.setText(banner.toString())
}
}
这样的方案似乎简洁了一点,对于业务层来说,我只需要关心banner不为空的结果即可,但其他网络异常就无法获知了。不够健全。
方案3:Retrofit自定义CallAdapter
在这里我要思考一下,我想要达到什么样的效果呢?
比如
lifecycleScope.launch {
val result = service.banner()
//代码1
if (result is Success) {
tvResult.setText(result.body().toString())
}
}
service.banner()返回的是一个结果,这个结果包括了网络异常,业务失败,业务成功这三个场景,而不会在异常情况时抛出异常,进而通过代码1判断是否业务成功。
确定方案可行
考虑一下,Retrofit是否能够处理这样的情况?当然是可以的,改变函数返回结果,那就要自定义CallAdapter,这是Retrofit特性,也是优势所在。
执行方案
想要自定义CallAdapter,先来看看CallAdapterFactory的结构,想要了解CallAdapterFactory的结构,那么从Retrofit自带的DefaultCallAdapterFactory入手是再好不过的了。
以下是部分DefaultCallAdapterFactory源码
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
private final @Nullable Executor callbackExecutor;
DefaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
this.callbackExecutor = callbackExecutor;
}
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
//代码1
if (getRawType(returnType) != Call.class) {
return null;
}
//代码2
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException(
"Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
//代码3
final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);
final Executor executor =
Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
? null
: callbackExecutor;
//代码4
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
}
};
}
}
DefaultCallAdapterFactory继承CallAdapter.Factory,并且重写了get()方法,最终得到一个CallAdapter匿名内部类。
来一步步分析一下
代码1,getRawType(returnType),这个returnType是传入进来的,之前我们分析原理的时候知道,这个returnType在非suspend方法时为Call。getRawType()是获取最外层的Call,这里是匹配上的。
代码2,returnType instanceof ParameterizedType表示这个Call是一个参数化类型吗?也就是带有泛型吗?很显然是带的。
代码3,既然是参数化类型,那就拿具体的类型吧,就是Banner。
代码4,这个Banner给了匿名内部类的responseType()方法,并且get()方法返回了CallAdapter,表示,这种Call类型的,被DefaultCallAdapterFactory匹配上了,就用它了。
那我们来模仿一下
自定义ApiResultCallAdapterFactory
class ApiResultCallAdapterFactory :
CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
return null
}
}
如上,我们定义了一个ApiResultCallAdapterFactory,重写get()方法,这才想起来,我们需要在get()方法中判断自己需要的类型,但是这个类型还没有创建出来呢,并且这个类是个包裹网络请求的结果的,比如业务成功,业务失败之类的。我们可以用到kotlin的特性之一,密封类,可以理解为这个类的子类是有限,可知的。
来创建一个ApiResult
sealed class ApiResult<T> {
data class BizSuccess<T>(val data: T?) : ApiResult<T>()
data class BizError(val errorCode: Int, val errorMsg: String) : ApiResult<Nothing>()
data class OtherError(val throwable: Throwable) : ApiResult<Nothing>()
}
我们关注的是BizSuccess,包裹的data是请求结果,如此一来,接口怎么定义了?如下所示
interface WanAndroidSer
@GET("banner/json")
suspend fun banner(): ApiResult<Banner>
}
重写get()方法
现在我们知道想要啥类型了,来判断一下吧
class ApiResultCallAdapterFactory :
CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
//代码1
if (getRawType(returnType) != ApiResult.class) {
return null;
}
}
}
先写代码1,我们照抄DefaultCallAdapterFactory,将原来的Call.class改成ApiResult.class就行了,是吗?
理论上是对的,但实际不是。为什么?可以从之前Retrofit对kotlin协程支持的原理来看
HttpServiceMethod#parseAnnotations()源码
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
if (isKotlinSuspendFunction) {
...判断逻辑省略
//代码1
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
}
//代码2
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
//代码3
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
代码1,如果是suspend方法,那么会将responseType和Call.class封装成ParameterizedTypeImpl,作为新的adapterType。简单理解一下就是将responseType封装成Call,也就是将ApiResult封装成Call<ApiResult>,然后再执行代码2,createCallAdapter,查找CallAdapter。
既然被封装成了Call,为何不会匹配到DefaultCallAdapterFactory?
因为我们自定义的ApiResultCallAdapterFactory优先级比较高,排在集合的前面,遍历时自然先匹配ApiResultCallAdapterFactory,一旦匹配成功就会return
所以get()方法应该这样写
class ApiResultCallAdapterFactory :
CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
//判断是否为Call<ApiResult<Banner>>
if (getRawType(returnType) != Call::class.java) return null
//参数化类型
if (!(returnType is ParameterizedType)) {
throw new IllegalArgumentException(
"return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
//拿到ApiResult<Banner>
val apiResultType = Utils.getParameterUpperBound(0, returnType)
//判断是否为ApiResult
if (getRawType(apiResultType) != ApiResult::class.java) return null
//ApiResult是参数化类型
if (!(apiResultType is ParameterizedType)) {
throw new IllegalArgumentException(
"return type must be parameterized");
}
//拿到ApiResult<Banner>中的Banner
val dataType = getParameterUpperBound(0, apiResultType)
//Banner传给ApiResultCallAdapter
return ApiResultCallAdapter(dataType)
}
}
代码注释如上,最终返回的ApiResultCallAdapter,是我们自定的CallAdapter。接下来再看看这个ApiResultCallAdapter
自定义ApiResultCallAdapter
还是先来模仿下DefaultCallAdapterFactory的CallAdapter写法
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
}
};
}
}
首先得是CallAdapter子类,然后重写responseType和adapt方法,那我们自定义一下
class ApiResultCallAdapter(
private val responseType: Type
) : CallAdapter<Any, Call<Any>> {
override fun responseType(): Type = responseType
override fun adapt(call: Call<Any>): Call<Any> {
return ApiResultCall(call)
}
}
这个responseType()用来指定我们想要的解析结果,目前我们就一个GsonConverterFactory,这个GsonConverterFactory根据我们传入的responseType解析成Banner,正好符合要求。
这个adapt()之前分析原理的时候已经了解过了。传进来的call对象是OkHttpCall
调用链是HttpServiceMethod#invoke() -> SuspendForBody#adapt() -> ApiResultCallAdapter#adapt()。
最终adapt()方法中返回了ApiResultCall(),也是我们自定义的。
自定义ApiResultCall
我们还是模仿DefaultCallAdapterFactory,来看看它的Call对象,之前原理分析过是ExecutorCallbackCall
static final class ExecutorCallbackCall<T> implements Call<T> {
....
@Override
public void enqueue(final Callback<T> callback) {
Objects.requireNonNull(callback, "callback == null");
delegate.enqueue(
new Callback<T>() {
@Override
public void onResponse(Call<T> call, final Response<T> response) {
...切换到主线程回调
}
@Override
public void onFailure(Call<T> call, final Throwable t) {
...切换到主线程回调
}
});
}
@Override
public boolean isExecuted() {
return delegate.isExecuted();
}
@Override
public Response<T> execute() throws IOException {
return delegate.execute();
}
@Override
public void cancel() {
delegate.cancel();
}
@Override
public boolean isCanceled() {
return delegate.isCanceled();
}
@SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone.
@Override
public Call<T> clone() {
return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone());
}
@Override
public Request request() {
return delegate.request();
}
@Override
public Timeout timeout() {
return delegate.timeout();
}
}
ExecutorCallbackCall不会在SuspendForBody调用,但有借鉴意义。
ExecutorCallbackCall继承Call,并且重写的方法很多,不过我们重点关注的还是enqueue()方法,因为我们自定义的ApiResultCall#enqueue()方法最终会在SuspendForBody中被调用。并且在enqueue()中传入的回调写法很重要,因为这会影响异常是否抛出,业务是否成功。
先来分析一下KotlinExtensions#await()
KotlinExtensions#await()
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
...
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
....异常信息
//代码1
continuation.resumeWithException(e)
} else {
//代码2
continuation.resume(body)
}
} else {
//代码3
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
//代码4
continuation.resumeWithException(t)
}
})
}
}
之前讲过这个KotlinExtensions#await()方法,是最终开启协程并请求网络的关键步骤。
聚焦一下我在代码中的标记,只有走代码2才能不抛出异常,否则代码1,3,4都会抛出异常。代码1抛出异常是因为我要求返回值不能为空,所以Retrofit发现不符合要求,直接抛出异常,我们就按照这种设定来吧,反正规避掉代码1,3,4就行了。
既然回调逻辑搞清楚了,那么我们可以自己编写了。
ApiResultCall编写
class ApiResultCall(
private val delegate: Call<Any>
) : Call<Any> {
override fun enqueue(callback: Callback<Any>) {
//delegate为OkHttpCall
delegate.enqueue(object : Callback<Any> {
override fun onResponse(call: Call<Any>, response: Response<Any>) {
if (response.isSuccessful) {
//经过GsonConverterFactory转换,response.body()变成了Banner
val body = response.body()
if (body == null) {
//代码1
callback.onResponse(this@ApiResultCall,Response.success(ApiResult.BizError(-1,"错误")))
} else {
//代码2
callback.onResponse(this@ApiResultCall,Response.success(ApiResult.BizSuccess(body)))
}
} else {
//参考代码1
}
}
override fun onFailure(call: Call<Any>, t: Throwable) {
//代码3
callback.onResponse(this@ApiResultCall, Response.success(ApiResult.OtherError(t)))
}
})
}
}
代码1,2,3的callback就是KotlinExtensions#await()中的回调,将直接影响结果走向。这里很重要!
所以可以看到,我们这里的处理就是代码1,2,3都回调onResponse()方法,并且传入的body也是Response.success(body)或者Response.success(ApiResult.BizError(-1,"错误")),目的就是在KotlinExtensions#await()中永远走continuation.resume(body)逻辑,即不会抛出异常。当然对于body的处理和判断可以自定义,也可以定义自己的errorCode的含义,最后我们将解析成功的Banner重新封装成ApiResult,符合我们想要的结果。ApiResultCall中的其他方法重写,照着ExecutorCallbackCall写就可以了
再强调一下注意点
- 回调onResponse()方法
- 传入Response.success
缺一不可。
这里就有几个问题了。
-
Response.success()是什么?
这个Response是retrofit2的Response,success()是一个封装Response的方法,可以看一看
public static <T> Response<T> success(@Nullable T body) { return success( body, new okhttp3.Response.Builder() // .code(200) .message("OK") .protocol(Protocol.HTTP_1_1) .request(new Request.Builder().url("http://localhost/").build()) .build()); }
就是重新封装,对应的还有Response.error(),当然了Response.error()是不能用的,否则同样会抛出异常,因为在KotlinExtensions#await()中会走代码3,因为response.isSuccessful是false。如果我们不愿意用Response.success(),那也可以自己写。
-
Response.success(Any())可以吗?
虽然例子没有写Response.success(Any()),实际上,如果我们使用Response.success(Any())会crash,Response.success()方法没有问题,问题是Any(),我们不能传入Any(),否则会在continuation.resume(body)中对body进行强转时失败,告诉你Any()不能强转为ApiResult。
结论
最终,我们完成了对kotlin协程异常的封装,无论成功失败都不会抛出异常,我们要做的就是在结果中判断成功和失败。
新的优化点
现在我们再来看一下实际业务场景。wanAndroid的Banner接口返回数据是这样的形式
{
"data": [...],
"errorCode": 0,
"errorMsg": ""
}
这样的结构,在我们实际的开发中很常见,往往是需要经过errorCode == 0的判断才可以进行业务处理,所以我们的代码就变成了这样。
lifecycleScope.launch {
val result = service.banner()
if (result is ApiResult.BizSuccess) {
if (result.data.errorCode == 0) {
tvResult.setText(result.data.data.toString())
}
}
}
并且所有解析的Bean都需要包含重复的errorCode,针对这样的业务场景,我们该怎么优化呢。或者我们想要什么样的代码?我觉得应该是下面这样的
lifecycleScope.launch {
val result = service.banner()
if (result is ApiResult.BizSuccess) {
tvResult.setText(result.data.toString())
}
}
Banner实体类如下,不再包含errorCode,errorMsg
data class Banner(
val desc: String = "", // 一起来做个App吧
val id: Int = 0, // 10
.....
)
请求如下
interface WanAndroidSer
@GET("banner/json")
//这里从ApiResult<Banner>改为ApiResult<List<Banner>>
suspend fun banner(): ApiResult<List<Banner>>
}
ApiResult如下
sealed class ApiResult<T> {
//BizSuccess增加了errorCode,errorMsg,也就是将原来的解析Bean中的字段移到这里
data class BizSuccess<T>(val errorCode: Int, val errorMsg: String, val data: T?) : ApiResult<T>()
data class BizError(val errorCode: Int, val errorMsg: String) : ApiResult<Nothing>()
data class OtherError(val throwable: Throwable) : ApiResult<Nothing>()
}
接下来,怎么改代码。
首先是ApiResultCallAdapterFactory的get()方法,我们能拿到需要的参数类型,ApiResult,List都可以拿到,但是ApiResultCallAdapter中的responseType()传什么呢?
如果是传ApiResult<List>,那么解析时会报错,sealed修饰的类,不能被初始化。
如果传List,那么现有的GsonConverterFactory不能帮我们解析成想要的样子。
怎么办?
这里想到两个方法:
- 对List进行包装,改为BizSuccess<List>,或者新建一个BaseResponse替代BizSuccess也可,主要是为了让GsonConverterFactory能够解析。
- 自定义GsonConverterFactory解析,按照我们自己的解析方案来解析结果。
针对方法1,借鉴了这一行代码,Retrofit在suspend方法中将responseType转化为Call
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
所以,我写了这样的代码
returnType = new Utils.ParameterizedTypeImpl(null, ApiResult.BizSuccess.class, responseType);
Utils.ParameterizedTypeImpl是限制外部访问的,我就将这一系列代码拷贝过来。然后将returnType传给ApiResultCallAdapter。但经过很多尝试,由于对反射代码了解不透彻,最终没有成功。有能力的朋友可以再尝试一下
这里介绍方法2,相对来说比较简单一点。
自定义GsonConverterFactory
先来确定下ApiResultCallAdapter中的responseType()传什么。
毫无疑问是List,ApiResultCallAdapterFactory的get()写法维持不变
我们还是先来看看原版GsonConverterFactory是怎么写的
public final class GsonConverterFactory extends Converter.Factory {
//由GsonConverterFactory#create(Gson)初始化
private final Gson gson;
@Override
public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
//这里拿到了List<Banner>的adapter
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
//代码2
return new GsonResponseBodyConverter<>(gson, adapter);
}
@Override
public Converter<?, RequestBody> requestBodyConverter() {
.....
}
}
拿到List的adapter之后,返回了一个GsonResponseBodyConverter,我们来看一下这个类
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
//代码1
JsonReader jsonReader = gson.newJsonReader(value.charStream());
try {
//代码2
T result = adapter.read(jsonReader);
....
return result;
} finally {
value.close();
}
}
}
主要的方法就是GsonResponseBodyConverter#convert()方法,这个convert方法会在OkHttpCall#parseResponse()方法中调用,之前分析原理时讲过,用于解析请求结果。
代码1,直接将结果放到了JsonReader中,成了一个IO流
代码2,用adapter进行转换。但是这里的adapter是List的adapter,所以,我们自定义的时候需要分段解析,直接来看代码吧。
@Override
public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
jsonReader.beginObject();
int errorCode = -1;
String errorMsg = "";
T data = null;
while (jsonReader.hasNext()) {
String nextName = jsonReader.nextName();
if (TextUtils.equals(nextName, "errorCode")) {
errorCode = jsonReader.nextInt();
} else if (TextUtils.equals(nextName, "errorMsg")) {
errorMsg = jsonReader.nextString();
} else if (TextUtils.equals(nextName, "data")) {
data = adapter.read(jsonReader);
}
}
jsonReader.endObject();
return ((T) new ApiResult.BizSuccess(errorCode, errorMsg, data));
}
单独解析errorCode和errorMsg后,data数据依旧交给adapter解析,这样就可以了,最终返回一个ApiResult.BizSuccess,因为这个ApiResult.BizSuccess结构比较完整,所以用它来做最后的结果,相当于BaseResponse之前也说过。
不过,这只是示例代码,逻辑并不严谨,比如原版convert方法中最终会value.close();,包括其他解析节点判断等等。
回调的处理
最终,我们再回到ApiResultCallAdapterFactory中,看看ApiResultCall对回调的处理。
override fun enqueue(callback: Callback<Any>) {
delegate.enqueue(object : Callback<Any> {
override fun onResponse(call: Call<Any>, response: Response<Any>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
callback.onResponse(this@ApiResultCall,Response.success(ApiResult.BizError(-1,"错误")))
} else {
//代码1
if (body is ApiResult.BizSuccess<*> && body.errorCode == 0) {
callback.onResponse(this@ApiResultCall, Response.success(body))
}
}
} else {
....
}
}
...
})
}
看代码1,如果body为ApiResult.BizSuccess,并且body.errorCode == 0,那么才表示成功。
所以我们在业务层就可以这样判断了
lifecycleScope.launch {
val result = service.banner()
//这个BizSuccess就包含了body.errorCode == 0
if (result is ApiResult.BizSuccess) {
tvResult.setText(result.data.toString())
}
}
代码已上传到Github(包含第四章内容,建议看完第四章):github.com/lt19931203/…
完结。
继续看第四章-支持kotlin空安全之自定义Converter
参考链接: