Skip to content

Instantly share code, notes, and snippets.

@Vivekban
Last active January 20, 2024 00:47
Show Gist options
  • Save Vivekban/819addc7fd8e86a198a9d206385073d9 to your computer and use it in GitHub Desktop.
Save Vivekban/819addc7fd8e86a198a9d206385073d9 to your computer and use it in GitHub Desktop.
Retrofit CallAdapter to handle the API calls errors and success states at single source.
package com.vivek.githubapisample.api
import com.vivek.githubapisample.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
/**
* This class will be responsible to provide all kind Retrofit Client for making networking calls.
*/
object ApiClient {
/**
* This method will provide the Retrofit client with the given base url By default it will
* logs all network calls in debug mode
*
* @param baseUrl The base url of the api
* @return The Retrofit client
*/
fun getClient(baseUrl: String): Retrofit {
val logger =
HttpLoggingInterceptor().apply {
level = when (BuildConfig.DEBUG) {
true -> HttpLoggingInterceptor.Level.BODY
false -> HttpLoggingInterceptor.Level.NONE
}
}
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(ResultCallAdapterFactory())
.build()
}
}
package com.vivek.githubapisample.common.data
import androidx.compose.runtime.Immutable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/**
* A generic class that holds a value with its loading status.
*/
@Immutable
sealed class AppResult<out T> {
/**
* Represents a successful outcome.
*
* @param data The encapsulated value.
*/
data class Success<T>(val data: T) : AppResult<T>()
/**
* Represents a failed outcome.
*
* @param exception The encapsulated [Throwable] exception.
*/
data class Error(val exception: Throwable? = null) : AppResult<Nothing>()
/**
* Represents a loading state.
*/
data object Loading : AppResult<Nothing>()
/**
* Returns `true` if this instance represents a successful outcome.
*/
fun isSuccess(): Boolean = this is Success
/**
* Returns `true` if this instance represents a failed outcome.
*/
fun isError(): Boolean = this is Error
/**
* Returns `true` if this instance represents a failed outcome.
*/
fun isLoading(): Boolean = this is Loading
/**
* Returns the encapsulated value if this instance represents [success][AppResult.isSuccess] or `null`
* if it is [failure][AppResult.isError].
*/
fun getOrNull(): T? =
when (this) {
is Success -> this.data
else -> null
}
/**
* Returns the encapsulated [Throwable] exception if this instance represents [failure][isError] or `null`
* if it is [success][isSuccess].
*/
fun exceptionOrNull(): Throwable? =
when (this) {
is Error -> this.exception
else -> null
}
}
inline val Throwable.asAppResult get() = AppResult.Error(this)
package com.vivek.githubapisample.api
import com.vivek.githubapisample.common.data.AppResult
import com.vivek.githubapisample.common.data.ErrorResponse
import com.vivek.githubapisample.common.data.asAppResult
import com.vivek.githubapisample.common.data.asResult
import okhttp3.Request
import okhttp3.ResponseBody
import okio.Timeout
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Converter
import retrofit2.Response
import retrofit2.Retrofit
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import javax.net.ssl.HttpsURLConnection
/**
* Custom Retrofit CallAdapter to handle the API calls errors and success states, by this we can
* get response in form of [AppResult]
*
* For more details about it check here
* [Medium](https://proandroiddev.com/create-retrofit-calladapter-for-coroutines-to-handle-response-as-states-c102440de37a)
*/
class AppResultCallAdapter<R>(
private val responseType: Type,
private val errorBodyConverter: Converter<ResponseBody, ErrorResponse>
) : CallAdapter<R, Call<AppResult<R>>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<R>): Call<AppResult<R>> {
return AppResultCall(call, errorBodyConverter)
}
private class AppResultCall<R>(
private val delegate: Call<R>,
private val errorConverter: Converter<ResponseBody, ErrorResponse>
) : Call<AppResult<R>> {
override fun enqueue(callback: Callback<AppResult<R>>) {
delegate.enqueue(object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
val body = response.body()
val code = response.code()
val error = response.errorBody()
val result = if (response.isSuccessful) {
if (body != null) {
AppResult.Success(body)
} else {
AppException.EmptyBody().asAppResult
}
} else if (code == HttpsURLConnection.HTTP_NOT_FOUND) {
AppException.NotFound().asAppResult
} else {
val errorBody = when {
error == null -> null
error.contentLength() == 0L -> null
else -> try {
errorConverter.convert(error)?.message
} catch (ex: Exception) {
ex.message
}
}
errorBody?.let {
AppException.ApiError(errorBody, code).asAppResult
} ?: AppException.ApiError(null, code = code).asAppResult
}
callback.onResponse(this@AppResultCall, Response.success(result))
}
override fun onFailure(call: Call<R>, t: Throwable) {
val networkResponse = when (t) {
is IOException -> AppException.NetworkError(t)
else -> AppException.Error(t.message, t)
}
callback.onResponse(
this@AppResultCall,
Response.success(AppResult.Error(networkResponse))
)
}
})
}
// Other methods delegate to the original Call
override fun execute(): Response<AppResult<R>> {
throw UnsupportedOperationException("execute not supported")
}
override fun isExecuted(): Boolean = delegate.isExecuted
override fun cancel() = delegate.cancel()
override fun isCanceled() = delegate.isCanceled
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
override fun clone(): Call<AppResult<R>> {
return AppResultCall(delegate.clone(), errorConverter)
}
}
}
class AppResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// suspend functions wrap the response type in `Call`
if (Call::class.java != getRawType(returnType)) {
return null
}
// check first that the return type is `ParameterizedType`
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<AppResult<<Foo>> or Call<AppResult<out Foo>>"
}
// get the response type inside the `Call` type
val responseType = getParameterUpperBound(0, returnType)
// if the response type is not ApiResponse then we can't handle this type, so we return null
if (getRawType(responseType) != AppResult::class.java) {
return null
}
// the response type is ApiResponse and should be parameterized
check(responseType is ParameterizedType) { "Response must be parameterized as AppResult<Foo> or AppResult<out Foo>" }
val successBodyType = getParameterUpperBound(0, responseType)
val errorBodyConverter =
retrofit.nextResponseBodyConverter<ErrorResponse>(
null,
ErrorResponse::class.java,
annotations
)
return AppResultCallAdapter<Any>(successBodyType, errorBodyConverter)
}
}
package com.vivek.githubapisample.common.data
import com.squareup.moshi.JsonClass
/** Error response comes in case of Api failure (non 2xx code) */
@JsonClass(generateAdapter = true)
data class ErrorResponse(
val message: String,
)
package com.vivek.githubapisample.repo.data
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
/**
* Implementation of [RepoRemoteSource] uses retrofit to perform Network operation to
* fetch repo related information
*/
interface RepoService : RepoRemoteSource {
/**
* Get a list of repositories for a given username as [AppResult]
*
* @param username the username of the user
* @param page the page number of the results
* @param perPage the number of results per page
* @return a response containing a list of repositories
*/
@GET("users/{username}/repos")
override suspend fun getRepositoryByUsername(
@Path("username") username: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int,
): AppResult<List<RepoDto>>
companion object Factory {
/**
* Create a new instance of [RepoService] using the given [Retrofit] instance
*
* @param retrofit the retrofit instance to use
* @return a new instance of [RepoService]
*/
fun create(retrofit: Retrofit): RepoService {
return retrofit.create(RepoService::class.java)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment