Retrofit第三章-搭配kotlin协程更方便的自定义CallAdapter

3,312 阅读12分钟

点击这里查看第一章

点击这里查看第二章

在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写就可以了

再强调一下注意点

  1. 回调onResponse()方法
  2. 传入Response.success

缺一不可。

这里就有几个问题了。

  1. 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(),那也可以自己写。

  2. 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不能帮我们解析成想要的样子。

怎么办?

这里想到两个方法:

  1. 对List进行包装,改为BizSuccess<List>,或者新建一个BaseResponse替代BizSuccess也可,主要是为了让GsonConverterFactory能够解析。
  2. 自定义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

参考链接:

1.blog.yujinyan.me/posts/kotli…

2.blog.csdn.net/taotao11012…