Last active
March 24, 2023 16:10
-
-
Save timusus/df8d69df245759060acf0ad2f49a8872 to your computer and use it in GitHub Desktop.
Retrofit Error Handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
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
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
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
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