Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a call adapter for suspending functions in Retrofit?

I need to create a retrofit call adapter which can handle such network calls:

@GET("user") suspend fun getUser(): MyResponseWrapper<User> 

I want it to work with Kotlin Coroutines without using Deferred. I have already have a successful implementation using Deferred, which can handle methods such as:

@GET("user") fun getUser(): Deferred<MyResponseWrapper<User>> 

But I want the ability make the function a suspending function and remove the Deferred wrapper.

With suspending functions, Retrofit works as if there is a Call wrapper around the return type, so suspend fun getUser(): User is treated as fun getUser(): Call<User>

My Implementation

I have tried to create a call adapter which tries to handle this. Here is my implementation so far:

Factory

class MyWrapperAdapterFactory : CallAdapter.Factory() {      override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {          val rawType = getRawType(returnType)          if (rawType == Call::class.java) {              returnType as? ParameterizedType                 ?: throw IllegalStateException("$returnType must be parameterized")              val containerType = getParameterUpperBound(0, returnType)              if (getRawType(containerType) != MyWrapper::class.java) {                 return null             }              containerType as? ParameterizedType                 ?: throw IllegalStateException("MyWrapper must be parameterized")              val successBodyType = getParameterUpperBound(0, containerType)             val errorBodyType = getParameterUpperBound(1, containerType)              val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(                 null,                 errorBodyType,                 annotations             )              return MyWrapperAdapter<Any, Any>(successBodyType, errorBodyConverter)         }         return null     } 

Adapter

class MyWrapperAdapter<T : Any>(     private val successBodyType: Type ) : CallAdapter<T, MyWrapper<T>> {      override fun adapt(call: Call<T>): MyWrapper<T> {         return try {             call.execute().toMyWrapper<T>()         } catch (e: IOException) {             e.toNetworkErrorWrapper()         }     }      override fun responseType(): Type = successBodyType } 
runBlocking {   val user: MyWrapper<User> = service.getUser() } 

Everything works as expected using this implementation, but just before the result of the network call is delivered to the user variable, I get the following error:

java.lang.ClassCastException: com.myproject.MyWrapper cannot be cast to retrofit2.Call      at retrofit2.HttpServiceMethod$SuspendForBody.adapt(HttpServiceMethod.java:185)     at retrofit2.HttpServiceMethod.invoke(HttpServiceMethod.java:132)     at retrofit2.Retrofit$1.invoke(Retrofit.java:149)     at com.sun.proxy.$Proxy6.getText(Unknown Source)     ... 

From Retrofit's source, here is the piece of code at HttpServiceMethod.java:185:

    @Override protected Object adapt(Call<ResponseT> call, Object[] args) {       call = callAdapter.adapt(call); // ERROR OCCURS HERE        //noinspection unchecked Checked by reflection inside RequestFactory.       Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];       return isNullable           ? KotlinExtensions.awaitNullable(call, continuation)           : KotlinExtensions.await(call, continuation);     } 

I'm not sure how to handle this error. Is there a way to fix?

like image 256
harold_admin Avatar asked Jun 06 '19 18:06

harold_admin


People also ask

What is call adapter in retrofit?

By default, this return wrapping is done as Call<TypedResponseClass> type. This action of returning from the background thread, which receives and prepares the result, to the Android UI thread is a call adapter! Basically, call adapters are sitting on top of Retrofit and offer the functionality to you as the developer.

What is call class in retrofit?

Think of Call as a simple class which wraps your API response and you need this class make an API call and provide listeners/callback to notify you with error and response , although if you use kotlin coroutines then after version 2.6.


1 Answers

Here is a working example of an adapter, which automatically wraps a response to the Result wrapper. A GitHub sample is also available.

// build.gradle  ... dependencies {     implementation 'com.squareup.retrofit2:retrofit:2.6.1'     implementation 'com.squareup.retrofit2:converter-gson:2.6.1'     implementation 'com.google.code.gson:gson:2.8.5' } 
// test.kt  ... sealed class Result<out T> {     data class Success<T>(val data: T?) : Result<T>()     data class Failure(val statusCode: Int?) : Result<Nothing>()     object NetworkError : Result<Nothing>() }  data class Bar(     @SerializedName("foo")     val foo: String )  interface Service {     @GET("bar")     suspend fun getBar(): Result<Bar>      @GET("bars")     suspend fun getBars(): Result<List<Bar>> }  abstract class CallDelegate<TIn, TOut>(     protected val proxy: Call<TIn> ) : Call<TOut> {     override fun execute(): Response<TOut> = throw NotImplementedError()     override final fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)     override final fun clone(): Call<TOut> = cloneImpl()      override fun cancel() = proxy.cancel()     override fun request(): Request = proxy.request()     override fun isExecuted() = proxy.isExecuted     override fun isCanceled() = proxy.isCanceled      abstract fun enqueueImpl(callback: Callback<TOut>)     abstract fun cloneImpl(): Call<TOut> }  class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Result<T>>(proxy) {     override fun enqueueImpl(callback: Callback<Result<T>>) = proxy.enqueue(object: Callback<T> {         override fun onResponse(call: Call<T>, response: Response<T>) {             val code = response.code()             val result = if (code in 200 until 300) {                 val body = response.body()                 Result.Success(body)             } else {                 Result.Failure(code)             }              callback.onResponse(this@ResultCall, Response.success(result))         }          override fun onFailure(call: Call<T>, t: Throwable) {             val result = if (t is IOException) {                 Result.NetworkError             } else {                 Result.Failure(null)             }              callback.onResponse(this@ResultCall, Response.success(result))         }     })      override fun cloneImpl() = ResultCall(proxy.clone()) }  class ResultAdapter(     private val type: Type ): CallAdapter<Type, Call<Result<Type>>> {     override fun responseType() = type     override fun adapt(call: Call<Type>): Call<Result<Type>> = ResultCall(call) }  class MyCallAdapterFactory : CallAdapter.Factory() {     override fun get(         returnType: Type,         annotations: Array<Annotation>,         retrofit: Retrofit     ) = when (getRawType(returnType)) {         Call::class.java -> {             val callType = getParameterUpperBound(0, returnType as ParameterizedType)             when (getRawType(callType)) {                 Result::class.java -> {                     val resultType = getParameterUpperBound(0, callType as ParameterizedType)                     ResultAdapter(resultType)                 }                 else -> null             }         }         else -> null     } }  /**  * A Mock interceptor that returns a test data  */ class MockInterceptor : Interceptor {     override fun intercept(chain: Interceptor.Chain): okhttp3.Response {         val response = when (chain.request().url().encodedPath()) {             "/bar" -> """{"foo":"baz"}"""             "/bars" -> """[{"foo":"baz1"},{"foo":"baz2"}]"""             else -> throw Error("unknown request")         }          val mediaType = MediaType.parse("application/json")         val responseBody = ResponseBody.create(mediaType, response)          return okhttp3.Response.Builder()             .protocol(Protocol.HTTP_1_0)             .request(chain.request())             .code(200)             .message("")             .body(responseBody)             .build()     } }  suspend fun test() {     val mockInterceptor = MockInterceptor()     val mockClient = OkHttpClient.Builder()         .addInterceptor(mockInterceptor)         .build()      val retrofit = Retrofit.Builder()         .baseUrl("https://mock.com/")         .client(mockClient)         .addCallAdapterFactory(MyCallAdapterFactory())         .addConverterFactory(GsonConverterFactory.create())         .build()      val service = retrofit.create(Service::class.java)     val bar = service.getBar()     val bars = service.getBars()     ... } ... 
like image 139
Valeriy Katkov Avatar answered Oct 08 '22 06:10

Valeriy Katkov