Retrofit进阶篇 (二) suspend适配Response、suspend适配Body、suspend适配自定义ApiResponse

390 阅读5分钟

你在项目中采用retrofit搭配协程suspend时可能会遇到以下问题:

1,使用协程 suspend 时,我的方法返回值应该是 Response 还是 Body

//返回 response,对应Retrofit中的Response
@GET("/path")
suspend fun getDataWithResponse(): Response<ResponseBody<<Info>>>
	
//返回 body,对应下面自定义的ResponseBody
@GET("/path")
suspend fun getDataWithBody(): ReponseBody<List<Info>> 

//自定义的通用服务端返回结构
class ResponseBody<T> {

    var errorMsg: String? = null

    var errorCode: Int? = 0

    var data: T? = null
}

2,使用协程 suspend 时,方法内部抛出异常,除了 try catch ,有没有其他优雅的处理方式?


@GET("/path")    //这里path设置一个错误路径
suspend fun getDataWithResponse(): Response<ResponseBody<<Info>>>
	
//获取数据        
private fun getData() {
    GlobalScope.launch {
        val retrofit = Retrofit.Builder()
            .baseUrl("http://www.wanandroid.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val apiProxy = retrofit.create(HotKeyApiInterface::class.java)
        
        try {
            //当请求url中的path错误时,这里会抛出HttpException异常
            val response = apiProxy.getDataWithResponse()
        } catch(e: Exception) {
        } 
    }
}

下面将依次讲解:

  1. 协程 suspend 如何适配 Response
  2. 协程 suspend 如何适配 Body
  3. 协程 supspend 如何适配自定义 ApiResponse

协程 suspend 适配 Response

先看 Retrofit 中 suspend 结合 Response 的用法:


interface HotKeyApiInterface {

    @GET("/hotey/json")
    suspend fun getDataWithResponse(): Response<ResponseBody<List<Info>>>
    
}
	
private fun retrofitResponse() {
    GlobalScope.launch {
        val retrofit = Retrofit.Builder()
                .baseUrl("http://www.wanandroid.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

        val apiProxy = retrofit.create(HotKeyApiInterface::class.java)

        val response: Response<ResponseBody<List<Info>>> = apiProxy.getDataWithResponse()

        val apiResult: ResponseBody<List<Info>>? = response?.body()
    }
}

重点关注 apiProxy.getDataWithResponse() 是如何得到 response 的。

根据上一篇文章的分析,执行 apiProxy.getDataWithResponse() 时,由于 apiProxy 是动态代理类,所以会执行到 retrofit#create 方法中的 InvocationHandler#invoke,看看它的源码:


public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              private final Platform platform = Platform.get();
              private final Object[] emptyArgs = new Object[0];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);   //关键点1
              }
            });
  }
  

关键点1处:loadServiceMethod(method).invoke(args) 的执行结果就是 apiProxy.getDataWithResponse() 的结果,所以我们需要重点关注 loadServiceMethod(method).invoke(args) 的执行流程。

首先 loadServiceMethod(method) 方法返回的类型是 ServiceMethod,ServiceMethod有一个抽象实现类 HttpServiceMethod,如下:


abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {

}

HttpServiceMethod 有三个实现类:


static final class CallAdapted<ResponseT, ReturnT> extends HttpServiceMethod<ResponseT, ReturnT> {

}
	
static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {

}
	  
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
		
}

上面的 loadServiceMethod(method) 方法会根据 method,也就是我们定义的接口的方法信息,来决定返回的 ServiceMethod 是 CallAdapted、SuspendForResponse 或 SuspendForBody,具体判断方式为它会根据定义的方法的返回类型、是否带 suspend 等 来决定,如:

  1. 当方法返回Call,不带 suspend,则 loadServiceMethod(method) 返回 CallAdapter,如:

@GET("/hotkey/json")
fun getDataWithCall(): Call<ResponseBody<List<Info>>>

2,当方法带 suspend,返回 Response,则 loadServiceMethod(method) 返回 SuspendForResponse,如:


@GET("/hotey/json")
suspend fun getDataWithResponse(): Response<ResponseBody<List<Info>>>
    
  1. 当方法带 suspend,返回自定义数据 bean,则 loadServiceMethod(method) 返回 SuspendForBody,如:

@GET("/hotey/json")
suspend fun getDataWithResponseBody(): ResponseBody<List<Info>>
    

回到本节开头,我们定义的方法为:


@GET("/hotey/json")
suspend fun getDataWithResponse(): Response<ResponseBody<List<Info>>>

所以:上面的 loadServiceMethod(method) 的返回值 SuspendForResponse。

SuspendForResponse 就是解决 suspend 适配 Response 的,前面讲了 loadServiceMethod(method).invoke(args) 方法的返回值就是apiProxy.getDataWithResponse()的返回值,所以我们接着看SuspendForResponse的invoke方法:


//SuspendForResponse#invoke(实现在它的父类HttpServiceMethod)		
@Override
final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);  //关键点2
  }

关键点2:看 SuspendForResponse#adapter:


	
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call);

      //noinspection unchecked Checked by reflection inside RequestFactory.
      Continuation<Response<ResponseT>> continuation =
          (Continuation<Response<ResponseT>>) args[args.length - 1];

      // See SuspendForBody for explanation about this try/catch.
      try {
        return KotlinExtensions.awaitResponse(call, continuation); //关键点3
      } catch (Exception e) {
        return KotlinExtensions.suspendAndThrow(e, continuation);
      }
    }
  }	

关键点3:利用协程先挂起当前程序,然后在协程中调用 call#enqueue 执行异步请求,最终拿到结果后恢复执行。

//KotlinExtensions#awaitResponse方法
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        continuation.resume(response)   //关键点4
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}

关键点4处:将 Response 返回回去,就是我们 apiResponse.getDataWithResponse 的执行结果。

协程 suspend 适配 Body

接着看看 Retrofit 中 suspend 结合 Body 的用法:


interface HotKeyApiInterface {

    @GET("/hotey/json")
    suspend fun getDataWithBody(): ResponseBody<List<Info>>
    
}
	
private fun retrofitBody() {
    GlobalScope.launch {
        val retrofit = Retrofit.Builder()
                .baseUrl("http://www.wanandroid.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

        val apiProxy = retrofit.create(HotKeyApiInterface::class.java)

        val body: ResponseBody<List<Info>> = apiProxy.getDataWithBody()

    }
}

根据前面一节的分析:apiProxy.getDataWithBody() 执行时,会执行到 InvocationHandler.invoke,然后执行到 loadServiceMethod(method).invoke(arg),而 suspend 搭配 Body 时 loadServiceMethod(method) 方法的返回类型为 SuspendForBody,所以 SuspendForBody.invoke 的执行结果就是 apiProxy.getDataWithBody() 的返回结果。

先看 SuspendForBody 的 invoke 方法:


//SuspendForBody#invoke(实现在HttpMethodService中)
 @Override
 final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);  //关键点1
  }

关键点1处,接着看 SuspendForBody 的 adapter 方法


@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
     call = callAdapter.adapt(call);

     //noinspection unchecked Checked by reflection inside RequestFactory.
     Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];

     // Calls to OkHttp Call.enqueue() like those inside await and awaitNullable can sometimes
     // invoke the supplied callback with an exception before the invoking stack frame can return.
     // Coroutines will intercept the subsequent invocation of the Continuation and throw the
     // exception synchronously. A Java Proxy cannot throw checked exceptions without them being
     // declared on the interface method. To avoid the synchronous checked exception being wrapped
     // in an UndeclaredThrowableException, it is intercepted and supplied to a helper which will
     // force suspension to occur so that it can be instead delivered to the continuation to
     // bypass this restriction.
     try {
       return isNullable
           ? KotlinExtensions.awaitNullable(call, continuation)  //关键点2
           : KotlinExtensions.await(call, continuation);
     } catch (Exception e) {
       return KotlinExtensions.suspendAndThrow(e, continuation);
     }
   }
   

关键点2处:先挂起当前函数,然后在协程中调用 Call#enqueue 执行异步请求,拿到返回结果后返回 reponse.body(),就是我们 apiProxy.getDataWithBody 的执行结果。


suspend fun <T : Any> Call<T?>.await(): T? {
   return suspendCancellableCoroutine { continuation ->
   continuation.invokeOnCancellation {
     cancel()
   }
   enqueue(object : Callback<T?> {
     override fun onResponse(call: Call<T?>, response: Response<T?>) {
       if (response.isSuccessful) {
         continuation.resume(response.body())
       } else {
         continuation.resumeWithException(HttpException(response))
       }
     }

     override fun onFailure(call: Call<T?>, t: Throwable) {
       continuation.resumeWithException(t)
     }
   })
 }
}

协程 Suspend 适配自定义 ApiResponse

我们用下面的方式进行网络请求,一切都正常:


interface HotKeyApiInterface {

   @GET("/hotey/json")  //关键点1
   suspend fun getDataWithResponse(): Response<ResponseBody<List<Info>>>
   
}
   
private fun retrofitResponse() {

       GlobalScope.launch {
           val retrofit = Retrofit.Builder()
               .baseUrl("http://www.wanandroid.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .build()

           val apiProxy = retrofit.create(HotKeyApiInterface::class.java)

           val response: Response<ResponseBody<List<Info>>>? = apiProxy.getDataWithResponse()

       }
}

但是如果我们故意把url中的路径写错(关键点1处):

   
@GET("/hotey/jsonxxxx")  
suspend fun getDataWithResponse(): Response<ResponseBody<List<Info>>>

再次进行请求时,apiProxy.getDataWithResponse 方法内部会抛出 HttpException 异常,会导致程序崩溃,这时我们需要通过 try catch 的方式将异常捕获,如下:


private fun retrofitResponse() {

       GlobalScope.launch {
           val retrofit = Retrofit.Builder()
               .baseUrl("www.wanandroid.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .build()

           val apiProxy = retrofit.create(HotKeyApiInterface::class.java)
   		
           val response:Response<ApiResult<List<Info>>>? = null
   		
           try {
               response = apiProxy.getDataWithResponse()
           } catch {
           }
        
    }
}
   

但是工程中所有这种情况都采用 try catc h的方式进行处理并不合适,因为这样处理会产生大量重复代码,这时我们就可以通过自定义适配器 CallAdapter 生成一个 自定义 ApiResponse 来解决这个问题。

//后续更新