Global error handling for REST requests.
TO DO: documentation and comments are missing for this recipe!
/**
* A generic class that can provide a resource backed only by the network.
*
*
* You can read more about it in the [Architecture
* Guide](https://developer.android.com/arch).
* @param <ResultType>
* @param <RequestType>
*/
abstract class NetworkBoundResource<RequestType> {
private val result = MediatorLiveData<Resource<RequestType>>()
init {
setValue(Resource.loading(null))
fetchFromNetwork()
}
@MainThread
private fun setValue(newValue: Resource<RequestType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork() {
val apiResponse = createCall()
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
when (response) {
is ApiSuccessResponse -> {
setValue(Resource.success(processResponse(response)))
}
is ApiEmptyResponse -> {
setValue(Resource.success(null))
}
is ApiErrorResponse -> {
onFetchFailed()
setValue(Resource.error(response.errorMessage, null))
}
}
}
}
private fun onFetchFailed() {
}
fun asLiveData() = result as LiveData<Resource<RequestType>>
@WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
@MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}
/**
* Common class used by API responses.
* @param <T> the type of the response object
*/
@Suppress("unused") // T is used in extending classes
sealed class ApiResponse<T> {
companion object {
fun <T> create(error: Throwable): ApiErrorResponse<T> {
return ApiErrorResponse(error.message ?: "unknown error")
}
fun <T> create(response: Response<T>): ApiResponse<T> {
return if (response.isSuccessful) {
val body = response.body()
if (body == null || response.code() == 204) {
ApiEmptyResponse()
} else {
ApiSuccessResponse(
body = body
)
}
} else {
val msg = response.errorBody()?.string()
val errorMsg = if (msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMsg ?: "unknown error")
}
}
}
}
/**
* separate class for HTTP 204 responses so that we can make ApiSuccessResponse's body non-null.
*/
class ApiEmptyResponse<T> : ApiResponse<T>()
data class ApiSuccessResponse<T>(
val body: T
) : ApiResponse<T>()
data class ApiErrorResponse<T>(val errorMessage: String) : ApiResponse<T>()
/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
enum class Status {
LOADING,
SUCCESS,
ERROR
}
// We need to add a call adapter to the Retrofit Builder
private val retrofit = Retrofit.Builder()
.baseUrl("https://lorem.ipsum/")
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.build()
class LiveDataCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != LiveData::class.java) {
return null
}
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
val rawObservableType = getRawType(observableType)
require(rawObservableType == ApiResponse::class.java) { "type must be a resource" }
require(observableType is ParameterizedType) { "resource must be parameterized" }
val bodyType = getParameterUpperBound(0, observableType)
return LiveDataCallAdapter<Any>(bodyType)
}
}
Usage
Sample code:
fun fetchProfile(): LiveData<Resource<User>> {
return object : NetworkBoundResource<User>() {
override fun createCall(): LiveData<ApiResponse<User>> {
return Api.userService.getProfile()
}
}.asLiveData()
}