Skip to content

Instantly share code, notes, and snippets.

@marcRDZ
Created March 29, 2022 08:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save marcRDZ/83737a5e5a5043cfeccd7b82031cd42f to your computer and use it in GitHub Desktop.
Save marcRDZ/83737a5e5a5043cfeccd7b82031cd42f to your computer and use it in GitHub Desktop.
A custom Retrofit CallAdapter.Factory to handle Response as Either
import arrow.core.Either
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* Custom [CallAdapter.Factory] to handle Retrofit [Response] through [Either] type
*
* Original idea taken from:
* https://proandroiddev.com/retrofit-calladapter-for-either-type-2145781e1c20
*/
internal class EitherCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) return null
check(returnType is ParameterizedType) { "Return type must be a parameterized type." }
val responseType = getParameterUpperBound(0, returnType)
if (getRawType(responseType) != Either::class.java) return null
check(responseType is ParameterizedType) { "Response type must be a parameterized type." }
val leftType = getParameterUpperBound(0, responseType)
if (getRawType(leftType) != ApiError::class.java) return null
val rightType = getParameterUpperBound(1, responseType)
return EitherCallAdapter<Any>(rightType)
}
}
private class EitherCallAdapter<R>(
private val successType: Type
) : CallAdapter<R, Call<Either<ApiError, R>>> {
override fun adapt(call: Call<R>): Call<Either<ApiError, R>> = EitherCall(call, successType)
override fun responseType(): Type = successType
}
class EitherCall<R>(
private val delegate: Call<R>,
private val successType: Type
) : Call<Either<ApiError, R>> {
override fun enqueue(callback: Callback<Either<ApiError, R>>) = delegate.enqueue(
object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
callback.onResponse(this@EitherCall, Response.success(response.toEither()))
}
override fun onFailure(call: Call<R>, throwable: Throwable) {
val error = when (throwable) {
is IOException -> NetworkError(throwable)
else -> UnknownApiError(throwable)
}
callback.onResponse(this@EitherCall, Response.success(Either.Left(error)))
}
}
)
override fun isExecuted(): Boolean = delegate.isExecuted
override fun clone(): Call<Either<ApiError, R>> = EitherCall(delegate.clone(), successType)
override fun isCanceled(): Boolean = delegate.isCanceled
override fun cancel() = delegate.cancel()
override fun execute(): Response<Either<ApiError, R>> =
throw UnsupportedOperationException()
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
private fun <R> Response<R>.toEither(): Either<ApiError, R> {
// Http error response (4xx - 5xx)
if (!isSuccessful) {
val errorBody = errorBody()?.string() ?: ""
return Either.Left(HttpError(code(), errorBody))
}
// Http success response with body
body()?.let { body -> return Either.Right(body) }
// if we defined Unit as success type it means we expected no response body
// e.g. in case of 204 No Content
return if (successType == Unit::class.java) {
@Suppress("UNCHECKED_CAST")
Either.Right(Unit) as Either<ApiError, R>
} else {
@Suppress("UNCHECKED_CAST")
Either.Left(UnknownError("Response body was null")) as Either<ApiError, R>
}
}
}
sealed class ApiError
data class HttpError(val code: Int, val body: String) : ApiError()
data class NetworkError(val throwable: Throwable) : ApiError()
data class UnknownApiError(val throwable: Throwable) : ApiError()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment