Skip to content

Instantly share code, notes, and snippets.

@timusus
Last active March 24, 2023 16:10
Show Gist options
  • Save timusus/df8d69df245759060acf0ad2f49a8872 to your computer and use it in GitHub Desktop.
Save timusus/df8d69df245759060acf0ad2f49a8872 to your computer and use it in GitHub Desktop.
Retrofit Error Handling
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.concurrent.Executor
class CallResultAdapterFactory(private val responseErrorMapper: ((Response<*>) -> Error?)? = null) : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (CallAdapter.Factory.getRawType(returnType) != CallResult::class.java) {
return null
}
if (returnType !is ParameterizedType) {
throw IllegalStateException("CallResult must have generic type (e.g., CallResult<ResponseBody>)")
}
val responseType = CallAdapter.Factory.getParameterUpperBound(0, returnType)
val callbackExecutor = retrofit.callbackExecutor()
return CallResultAdapter<Any>(responseType, callbackExecutor, responseErrorMapper)
}
private class CallResultAdapter<R> constructor(
private val responseType: Type,
private val callbackExecutor: Executor?,
private val responseErrorMapper: ((Response<*>) -> Error?)? = null
) : CallAdapter<R, CallResult<R>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<R>): CallResult<R> {
return CallResultMapper(call, callbackExecutor, responseErrorMapper)
}
}
}
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.util.concurrent.Executor
sealed class Result<out T> {
data class Success<out T>(val data: T?) : Result<T>()
data class Failure(val error: Error) : Result<Nothing>()
}
interface CallResult<T> {
fun enqueue(callback: (result: Result<T>) -> Unit)
fun clone(): CallResult<T>
fun isExecuted(): Boolean
fun execute(): Response<T>
}
/**
* Maps a [Call] to [CallResultMapper].
* */
internal class CallResultMapper<T>(private val call: Call<T>, private val callbackExecutor: Executor?, private val responseErrorMapper: ((Response<*>) -> Error?)? = null) : CallResult<T> {
override fun execute(): Response<T> {
return call.execute()
}
override fun enqueue(callback: (result: Result<T>) -> Unit) {
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (call.isCanceled) return
val result: Result<T> = if (response.isSuccessful) {
Result.Success(response.body())
} else {
Result.Failure(responseErrorMapper?.invoke(response) ?: RemoteServiceHttpError(response))
}
val runnable = {
callback(result)
}
callbackExecutor?.execute(runnable) ?: runnable()
}
override fun onFailure(call: Call<T>, t: Throwable) {
val error: Error = if (t is IOException) {
NetworkError()
} else {
UnexpectedError()
}
val runnable = {
callback(Failure(error))
}
callbackExecutor?.execute(runnable) ?: runnable()
}
})
}
override fun isExecuted(): Boolean {
return call.isExecuted
}
override fun clone(): CallResult<T> {
return CallResultMapper(call.clone(), callbackExecutor)
}
}
class UnexpectedError : Error() {
override fun toString(): String {
return "UnexpectedError"
}
}
/**
* Represents an error in which the server could not be reached.
*/
class NetworkError : Error() {
override fun toString(): String {
return "NetworkError"
}
}
/**
* An error response from the server.
*/
open class RemoteServiceError : Error() {
override fun toString(): String {
return "RemoteServiceError(message: $message)"
}
}
/**
* A Remote Service Error with a HttpStatus code.
*/
open class RemoteServiceHttpError(val response: Response<*>) : RemoteServiceError() {
val httpStatusCode = HttpStatusCode.values().firstOrNull { statusCode -> statusCode.code == response.code() } ?: Unknown
val isClientError: Boolean
get() = response.code() in 400..499
val isServerError: Boolean
get() = response.code() in 500..599
override fun toString(): String {
return "RemoteServiceHttpError" +
"\ncode: ${response.code()} (${httpStatusCode.name})" +
"\nmessage: ${super.message}"
}
}
enum class HttpStatusCode(val code: Int) {
Unknown(-1),
// Client Errors
BadRequest(400),
Unauthorized(401),
PaymentRequired(402),
Forbidden(403),
NotFound(404),
MethodNotAllowed(405),
NotAcceptable(406),
ProxyAuthenticationRequired(407),
RequestTimeout(408),
Conflict(409),
Gone(410),
LengthRequired(411),
PreconditionFailed(412),
PayloadTooLarge(413),
UriTooLong(414),
UnsupportedMediaType(415),
RangeNotSatisfiable(416),
ExpectationFailed(417),
ImATeapot(418),
MisdirectedRequest(421),
UnprocessableEntity(422),
Locked(423),
FailedDependency(424),
UpgradeRequired(426),
PreconditionRequired(428),
TooManyRequests(429),
RequestHeaderFieldsTooLarge(431),
UnavailableForLegalReasons(451),
// Server Errors
InternalServerError(500),
NotImplemented(501),
BadGateway(502),
ServiceUnavailable(503),
GatewayTimeout(504),
HttpVersionNotSupported(505),
VariantAlsoNegates(506),
InsufficientStorage(507),
LoopDetected(508),
NotExtended(510),
NetworkAuthenticationRequired(511);
}
@esQmo
Copy link

esQmo commented May 20, 2021

Hello. Any exemple as of how to use this in a real app? I'm trying to implement such behavior

@timusus
Copy link
Author

timusus commented May 20, 2021

Hi, yes sorry - this was going to be a Medium post but I never finished it. I've just published it.. Let me know if you'd like further assistance

https://timusus.medium.com/network-error-handling-on-android-with-retrofit-kotlin-draft-6614f58fa58d

@esQmo
Copy link

esQmo commented May 20, 2021

Hey, thanks for your reply. I read your post on Medium. But couldn't find a way to implement it into my project. It uses couritine and MVVM pattern (viewmodel, repository). I"m still a noob so simple things can look hard for me

@timusus
Copy link
Author

timusus commented May 20, 2021

I'm using a kind of variation of this in a couple of apps, including one with Coroutines. It's late here, but I'll see if I can provide an example for you tomorrow

@esQmo
Copy link

esQmo commented May 30, 2021

Waited for you, you've surely forgot. So here's a little reminder :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment