Skip to content

Instantly share code, notes, and snippets.

@julianfalcionelli
Created October 28, 2019 10:52
Show Gist options
  • Save julianfalcionelli/0b298e2e4ba82aa163d8906bcdda4618 to your computer and use it in GitHub Desktop.
Save julianfalcionelli/0b298e2e4ba82aa163d8906bcdda4618 to your computer and use it in GitHub Desktop.
Retrofit Manager + Offline Mode Support in Kotlin Version
import android.app.Application
import android.util.Log
import co.myapp.BuildConfig
import co.myapp.infrastructure.manager.InternetManager
import co.myapp.infrastructure.manager.interfaces.AuthenticationManager
import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.google.gson.Gson
import com.readystatesoftware.chuck.ChuckInterceptor
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
class RetrofitManager(
private val application: Application,
private val gson: Gson,
private val authenticationManager: AuthenticationManager,
private val internetManager: InternetManager
) {
companion object {
const val TAG = "RetrofitManager"
const val HEADER_CACHE_CONTROL = "Cache-Control"
const val HEADER_PRAGMA = "Pragma"
}
private var customOkHttpClient: OkHttpClient? = null
private var customCachedOkHttpClient: OkHttpClient? = null
private var customNonCachedOkHttpClient: OkHttpClient? = null
private var defaultOkHttpClient: OkHttpClient? = null
private var cache: Cache? = null
/**
* Returns a Custom Retrofit instance.
*/
// set your desired log level
var customRetrofit: Retrofit? = null
get() {
if (field == null) {
val logging = HttpLoggingInterceptor()
logging.level = Level.BODY
// Order Matters
val httpClient = OkHttpClient.Builder()
.addInterceptor(provideHeaderInterceptor())
.addInterceptor(provideOfflineCacheInterceptor())
.addInterceptor(ChuckInterceptor(application))
.addInterceptor(logging)
if (BuildConfig.DEBUG) {
httpClient.addNetworkInterceptor(StethoInterceptor())
}
httpClient
.addNetworkInterceptor(provideCacheInterceptor())
.cache(provideCache())
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
customOkHttpClient = httpClient.build()
field = Retrofit.Builder()
.baseUrl(RestConstants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
.client(customOkHttpClient!!)
.build()
}
return field
}
/**
* Returns a Custom Retrofit instance which only checks on Cache.
*/
// set your desired log level
var customCachedRetrofit: Retrofit? = null
get() {
if (field == null) {
val logging = HttpLoggingInterceptor()
logging.level = Level.BODY
val httpClient = OkHttpClient.Builder()
.addInterceptor(provideForcedOfflineCacheInterceptor())
.addInterceptor(logging)
if (BuildConfig.DEBUG) {
httpClient.addNetworkInterceptor(StethoInterceptor())
}
httpClient
.cache(provideCache())
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
customCachedOkHttpClient = httpClient.build()
field = Retrofit.Builder()
.baseUrl(RestConstants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
.client(customCachedOkHttpClient!!)
.build()
}
return field
}
/**
* Returns a Custom Retrofit instance with non-cache policy.
*/
// set your desired log level
var customNonCachedRetrofit: Retrofit? = null
get() {
if (field == null) {
val logging = HttpLoggingInterceptor()
logging.level = Level.BODY
// Order Matters
val httpClient = OkHttpClient.Builder()
.addInterceptor(provideHeaderInterceptor())
.addInterceptor(ChuckInterceptor(application))
.addInterceptor(logging)
if (BuildConfig.DEBUG) {
httpClient.addNetworkInterceptor(StethoInterceptor())
}
httpClient
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
customNonCachedOkHttpClient = httpClient.build()
field = Retrofit.Builder()
.baseUrl(RestConstants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
.client(customNonCachedOkHttpClient!!)
.build()
}
return field
}
/**
* Returns a Clean Retrofit instance.
*/
// set your desired log level
var retrofit: Retrofit? = null
get() {
if (field == null) {
val logging = HttpLoggingInterceptor()
logging.level = Level.BODY
val httpClient = OkHttpClient.Builder()
.addInterceptor(provideOfflineCacheInterceptor())
.addInterceptor(logging)
.addNetworkInterceptor(StethoInterceptor())
.addNetworkInterceptor(provideCacheInterceptor())
.cache(provideCache())
defaultOkHttpClient = httpClient.build()
field = Retrofit.Builder()
.baseUrl(RestConstants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(defaultOkHttpClient!!)
.build()
}
return field
}
init {
Stetho.initializeWithDefaults(application)
}
private fun provideCache(): Cache? {
if (cache == null) {
try {
cache = Cache(File(application.cacheDir, "http-cache"),
(10 * 1024 * 1024).toLong()) // 10 MB
} catch (e: Exception) {
Log.e(TAG, "Could not create Cache!")
}
}
return cache
}
private fun provideCacheInterceptor(): Interceptor {
return Interceptor { chain ->
val response = chain.proceed(chain.request())
val isOnline = internetManager.isOnline ?: false
val cacheControl: CacheControl = if (isOnline) {
CacheControl.Builder()
.maxAge(0, TimeUnit.SECONDS)
.build()
} else {
CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build()
}
response.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.header(HEADER_CACHE_CONTROL, cacheControl.toString())
.build()
}
}
private fun provideOfflineCacheInterceptor(): Interceptor {
return Interceptor { chain ->
var request = chain.request()
val isOnline = internetManager.isOnline ?: false
if (!isOnline) {
val cacheControl = CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build()
request = request.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build()
}
chain.proceed(request)
}
}
private fun provideForcedOfflineCacheInterceptor(): Interceptor {
return Interceptor { chain ->
var request = chain.request()
val cacheControl = CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build()
request = request.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build()
chain.proceed(request)
}
}
fun clean() {
clearHttpClient(defaultOkHttpClient)
clearHttpClient(customOkHttpClient)
clearHttpClient(customCachedOkHttpClient)
clearHttpClient(customNonCachedOkHttpClient)
customRetrofit = null
customCachedRetrofit = null
customNonCachedRetrofit = null
retrofit = null
if (cache != null) {
try {
cache!!.evictAll()
} catch (e: IOException) {
Log.e(TAG, "Error cleaning http cache")
}
}
cache = null
}
private fun clearHttpClient(okHttpClient: OkHttpClient?) {
okHttpClient?.dispatcher()?.cancelAll()
}
private fun provideHeaderInterceptor(): Interceptor {
return Interceptor { chain ->
var request = chain.request()
val accessToken = authenticationManager.getToken().onErrorReturnItem("").blockingGet()
val newRequest = if (!accessToken.isNullOrEmpty()) {
request.newBuilder()
.addHeader(RestConstants.HEADER_AUTH, RestConstants.HEADER_BEARER + accessToken)
.build()
} else {
request
}
chain.proceed(newRequest)
}
}
}
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.io.IOException
import java.lang.reflect.Type
class RxErrorHandlingCallAdapterFactory private constructor() : CallAdapter.Factory() {
private val original: RxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create()
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *> {
return RxCallAdapterWrapper(
retrofit, original.get(returnType, annotations, retrofit) as CallAdapter<out Any, *>
)
}
private class RxCallAdapterWrapper<R> internal constructor(
private val retrofit: Retrofit,
private val wrapped: CallAdapter<R, *>
) : CallAdapter<R, Any> {
override fun responseType(): Type {
return wrapped.responseType()
}
override fun adapt(call: Call<R>): Any {
val adaptedCall = wrapped.adapt(call)
if (adaptedCall is Completable) {
return adaptedCall.onErrorResumeNext {
throwable -> Completable.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Single<*>) {
return adaptedCall.onErrorResumeNext {
throwable -> Single.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Observable<*>) {
return adaptedCall.onErrorResumeNext {
throwable: Throwable -> Observable.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Maybe<*>) {
return adaptedCall.onErrorResumeNext {
throwable: Throwable -> Maybe.error(asRetrofitException(throwable))
}
}
throw RuntimeException("Observable Type not supported")
}
private fun asRetrofitException(throwable: Throwable): ServerException {
// We had non-200 http error
if (throwable is HttpException) {
val response = throwable.response()
return ServerException.httpError(
response.raw().request().url().toString(),
response, retrofit)
}
// A network error happened
return if (throwable is IOException) {
ServerException.networkError(throwable)
} else ServerException.unexpectedError(throwable)
// We don't know what happened. We need to simply convert to an unknown error
}
}
companion object {
fun create(): CallAdapter.Factory {
return RxErrorHandlingCallAdapterFactory()
}
}
}
import retrofit2.Response
import retrofit2.Retrofit
import timber.log.Timber
import java.io.IOException
class ServerException internal constructor(
message: String?,
/**
* The request URL which produced the error.
*/
val url: String?,
/**
* Response object containing status code, headers, body, etc.
*/
val response: Response<*>?,
/**
* The event kind which triggered this error.
*/
val kind: Kind,
exception: Throwable?,
/**
* The Retrofit this request was executed on.
*/
private val retrofit: Retrofit?
) : RuntimeException(message, exception) {
public var serverError: MyAppServerError? = null
/**
* Identifies the event kind which triggered a [ServerException].
*/
enum class Kind {
/**
* An [IOException] occurred while communicating to the server.
*/
NETWORK,
/**
* A non-200 HTTP status code was received from the server.
*/
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
init {
if (response != null) {
try {
serverError = getErrorBodyAs(MyAppServerError::class.java)
} catch (exception: IOException) {
Timber.d(exception)
}
}
}
/**
* HTTP response body converted to specified `type`. `null` if there is no
* response.
*
* @throws IOException if unable to convert the body to the specified `type`.
*/
@Throws(IOException::class)
fun <T> getErrorBodyAs(type: Class<T>): T? {
if (response?.errorBody() == null) {
return null
}
val converter = retrofit?.responseBodyConverter<T>(type,
arrayOfNulls(0))
return converter?.convert(response.errorBody()!!)
}
companion object {
fun httpError(url: String, response: Response<*>, retrofit: Retrofit): ServerException {
val message = response.code().toString() + " " + response.message()
return ServerException(message, url, response, Kind.HTTP, null, retrofit)
}
fun networkError(exception: IOException): ServerException {
return ServerException(exception.message, null, null, Kind.NETWORK, exception, null)
}
fun unexpectedError(exception: Throwable): ServerException {
return ServerException(exception.message, null, null, Kind.UNEXPECTED, exception, null)
}
}
}
@Kiran89kumar
Copy link

Do you have UT for call adpater?

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