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>
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?
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.
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.
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() ... } ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With