38

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?

harold_admin
  • 1,117
  • 2
  • 10
  • 20
  • 4
    Note that Retrofit 2.6.0 introduced built-in support for `suspend`. – CommonsWare Jun 06 '19 at 18:33
  • Yes, my question is regarding Retrofit 2.6.0. – harold_admin Jun 07 '19 at 01:50
  • 1
    "But I want the ability make the function a suspending function and remove the Deferred wrapper" -- then just add the `suspend` keyword. You do not need a `CallAdapter` or its factory. `suspend fun getUser(): MyResponseWrapper` will work directly with Retrofit 2.6.0. – CommonsWare Jun 07 '19 at 10:46
  • Are you saying that any arbitrary type can now be automatically adapted to by Retrofit automatically? So call adapters are not necessary now? My question raises the problem that with suspending functions, retrofit assumes the `Call` wrapper around the response type. This causes a crash with custom call adapters. I think I'll just raise an issue on Retrofit's repository. – harold_admin Jun 07 '19 at 17:02
  • "Are you saying that any arbitrary type can now be automatically adapted to by Retrofit automatically?" -- I am saying that you do not need a `CallAdapter` for `suspend` anymore (vs. using Jake's `Deferred` one previously). You will still need converters (Moshi, Gson, Jackson, whatever) for converting your Web service payloads to POKOs. I do not know what `MyResponseWrapper` is, so I cannot comment on whether you need something special in Retrofit for that. – CommonsWare Jun 07 '19 at 17:09
  • But, for example, I have `@GET("/gridpoints/{office}/{gridX},{gridY}/forecast") suspend fun getForecast( @Path("office") office: String, @Path("gridX") gridX: Int, @Path("gridY") gridY: Int ): WeatherResponse`, which works without `Deferred` and without a custom `CallAdapter`. – CommonsWare Jun 07 '19 at 17:10
  • I think adding more context here would help. I have a retrofit call adapter library based on `Deferred` from Kotlin Coroutines that allows you to write your API responses as `NetworkResponse`. Here's [a link to it](https://www.github.com/haroldadmin/coroutinesnetworkresponseadapter). While the existing version continues to work just fine, I wanted to see if I could add support for suspending functions. I ran into the described problem while trying to do that. – harold_admin Jun 07 '19 at 17:34
  • were you able to solve this? – Ruan_Lopes Sep 12 '19 at 22:54
  • There has been an answer posted by Valeriy [here](https://stackoverflow.com/a/57816819/7889026). I haven't been able to try it yet, but it looks like it should work. – harold_admin Sep 13 '19 at 06:59

3 Answers3

29

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()
    ...
}
...
Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
  • Thank you for this. I have tested it and it works. [My issue](https://github.com/square/retrofit/issues/3193) on Retrofit's repo still remains open in hope of an official sample. – harold_admin Sep 14 '19 at 10:24
  • 4
    Thanks for you solution! But it won't work if the response is a list of objects, e.g. Result>. To complete the solution, we should change `Type` as the input of ResultAdapter, instead of `Class`, so that _responseType()_ function also return a _type_. in _MyCallAdapterFactory_, change `ResultAdapter(getRawType(resultType))` to `ResultAdapter(resultType)` – qnd-S Oct 13 '19 at 10:29
  • Thank 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. – aaronmarino Oct 31 '19 at 16:18
  • 1
    Thanks @aaronmarino and qnd-S! I've updated the answer and the sample. – Valeriy Katkov Nov 01 '19 at 08:22
  • 1
    Glad that it helps. Happy coding! – qnd-S Nov 04 '19 at 08:43
  • This solution works, but if I cancel my coroutine would it cancel the request as well? @ValeriyKatkov – Roshaan Farrukh Nov 22 '19 at 13:28
  • 1
    This won't compile with Retrofit 2.8 because they added Call.timeout(). – amram99 May 27 '21 at 21:39
26

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.

Hakem Zaied
  • 14,061
  • 1
  • 23
  • 25
  • 4
    I 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. – harold_admin Jun 09 '19 at 06:15
  • 1
    You 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 the `response.errorBody()` if there was an error body if not then `response.message()` – Hakem Zaied Jun 15 '19 at 23:31
  • 2
    `suspend fun getUser(): Response` helped me. thanks! – Mirjalal Jun 20 '19 at 05:13
  • I am trying to create a wrapper too for this case (handle error directly with the adapter). Anyone get it working? – JavierSegoviaCordoba Jul 01 '19 at 23:45
  • 2
    `ApiResponse` does not work as a return type while the `suspend` keyword is in use, the call throws an error along the lines of `Unable to invoke no-args constructor for interface` – Daniel Wilson Nov 11 '19 at 11:37
  • 1
    Wow! Thanks for this, didn't know there was no need for a wrapper – Mayokun Dec 03 '19 at 04:02
  • You 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 – Niroshan Feb 13 '20 at 03:28
5

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)
    }
}
Mikael
  • 188
  • 1
  • 3
  • 1
    Thank 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. – harold_admin Jun 21 '19 at 15:16
  • @mikael any way this could be turned into an extension method? – maxbeaudoin Aug 09 '19 at 22:06