How to create a call adapter for suspending functions in Retrofit?
Solution 1
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()
...
}
...
Solution 2
When you use Retrofit 2.6.0
with coroutines you don't need a wrapper anymore. It should look like below:
@GET("user")
suspend fun getUser(): User
You don't need MyResponseWrapper
anymore, and when you call it, it should look like
runBlocking {
val user: User = service.getUser()
}
To get the retrofit Response
you can do the following:
@GET("user")
suspend fun getUser(): Response<User>
You also don't need the MyWrapperAdapterFactory
or the MyWrapperAdapter
.
Hope this answered your question!
Edit CommonsWare@ has also mentioned this in the comments above
Edit Handling error could be as follow:
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Where you just need to call create with the response as ApiResponse.create(response)
and it should return correct type. A more advanced scenario could be added here as well, by parsing the error if it is not just a plain string.
Solution 3
This question came up in the pull request where suspend
was introduced to Retrofit.
matejdro: From what I see, this MR completely bypasses call adapters when using suspend functions. I'm currently using custom call adapters for centralising parsing of error body (and then throwing appropriate exceptions), smilarly to the official retrofit2 sample. Any chance we get alternative to this, some kind of adapter that is injected between here?
It turns out this is not supported (yet?).
Source: https://github.com/square/retrofit/pull/2886#issuecomment-438936312
For error handling I went for something like this to invoke api calls:
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>): MyWrapper<T> {
return try {
val response = call.invoke()
when (response.code()) {
// return MyWrapper based on response code
// MyWrapper is sealed class with subclasses Success and Failure
}
} catch (error: Throwable) {
Failure(error)
}
}
harold_admin
Updated on November 01, 2020Comments
-
harold_admin over 3 years
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 usingDeferred
, 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, sosuspend fun getUser(): User
is treated asfun 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?
-
harold_admin almost 5 yearsI know I don't need a wrapper, but I want a wrapper. The wrapper I want to use provides an alternate way to handle an any errors that may have occurred in the network call.
-
Hakem Zaied almost 5 yearsYou can check if the response is successful as
response.isSuccessful
and then if it was failure you can handle the response body differently by parsing theresponse.errorBody()
if there was an error body if not thenresponse.message()
-
Mirjalal almost 5 years
suspend fun getUser(): Response<User>
helped me. thanks! -
harold_admin almost 5 yearsThank you for highlighting that comment. It looks like the developers of Retrofit are aware of this problem. I guess we will just have to wait and see what happens.
-
JavierSegoviaCordoba almost 5 yearsI am trying to create a wrapper too for this case (handle error directly with the adapter). Anyone get it working?
-
maxbeaudoin almost 5 years@mikael any way this could be turned into an extension method?
-
harold_admin over 4 yearsThank you for this. I have tested it and it works. My issue on Retrofit's repo still remains open in hope of an official sample.
-
qnd-S over 4 yearsThanks for you solution! But it won't work if the response is a list of objects, e.g. Result<List<Foo>>. To complete the solution, we should change
Type
as the input of ResultAdapter, instead ofClass<T>
, so that responseType() function also return a type. in MyCallAdapterFactory, changeResultAdapter(getRawType(resultType))
toResultAdapter<Any>(resultType)
-
aaronmarino over 4 yearsThank you @qnd-S. I just wasted several hours trying to figure out why my Type wasn't being registered in Gson's
registerTypeAdapter
and your solution fixed it for me. @Valeriy Katkov, please consider updating your answer. -
Valeriy Katkov over 4 yearsThanks @aaronmarino and qnd-S! I've updated the answer and the sample.
-
qnd-S over 4 yearsGlad that it helps. Happy coding!
-
Daniel Wilson over 4 years
ApiResponse
does not work as a return type while thesuspend
keyword is in use, the call throws an error along the lines ofUnable to invoke no-args constructor for interface
-
Roshaan Farrukh over 4 yearsThis solution works, but if I cancel my coroutine would it cancel the request as well? @ValeriyKatkov
-
Mayokun over 4 yearsWow! Thanks for this, didn't know there was no need for a wrapper
-
Niroshan over 4 yearsYou can't relay on response.message() for error. its totally useless if you want to serializer your error. "Error handling" is not something where you just return a "String" :P
-
amram99 almost 3 yearsThis won't compile with Retrofit 2.8 because they added Call.timeout().