Created
October 28, 2019 10:52
-
-
Save julianfalcionelli/0b298e2e4ba82aa163d8906bcdda4618 to your computer and use it in GitHub Desktop.
Retrofit Manager + Offline Mode Support in Kotlin Version
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 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) | |
} | |
} | |
} |
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 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() | |
} | |
} | |
} |
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.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) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do you have UT for call adpater?