-
-
Save Dadoufi/ce4a918cf458d929529f81ae73b5179b to your computer and use it in GitHub Desktop.
1. RetrofitException
2. RxErrorHandlingCallAdapterFactory
3. YourRetrofitBuilder
4. RetrofitBuilder
5. RetrofitManager
6. CookieManager
7. SingletonHolder
8. Injector
9. RetrofitHelpers
This file contains hidden or 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
/** | |
* Injector class implements Injection interface and lives in different flawors | |
*/ | |
object Injector: Injection { | |
override fun provideOkHttpClient(cache: Cache, config: OkHttpConfigurator): OkHttpClient { | |
val interceptor = HttpLoggingInterceptor() | |
interceptor.level = HttpLoggingInterceptor.Level.BODY | |
return OkHttpClient.Builder() | |
.addInterceptor(interceptor) | |
.cache(cache) | |
.apply { config() } | |
.build() | |
} | |
override fun provideMovieDetailRepository(application: Application): MovieDetailRepository { | |
val webService = RetrofitManager.createService(application, MovieWebService::class.java) | |
val movieDao = Room.databaseBuilder(application, MovieDatabase::class.java, "movie-database").build().movieDao() | |
return MovieDetailRepository(webService, movieDao) | |
} | |
} |
This file contains hidden or 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
/** | |
* Base class that puts together components, which configured in subclass | |
* in order to create Retrofit instance | |
*/ | |
abstract class RetrofitBuilder { | |
fun buildRetrofit(): Retrofit { | |
return Retrofit.Builder() | |
.apply { configRetrofit() } | |
.build() | |
} | |
fun buildGson(): Gson { | |
return GsonBuilder() | |
.apply { configGson() } | |
.create() | |
} | |
fun buildOkHttpClient(cacheDir: File, factory: (Cache) -> OkHttpClient): OkHttpClient { | |
val cacheSize = 10 * 1024 * 1024 // 10 MiB | |
val cache = Cache(cacheDir, cacheSize.toLong()) | |
return factory(cache) | |
} | |
abstract fun Retrofit.Builder.configRetrofit() | |
abstract fun GsonBuilder.configGson() | |
abstract fun OkHttpClient.Builder.configOkHttpClient() | |
} |
This file contains hidden or 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 okhttp3.ResponseBody | |
import retrofit2.Converter | |
import retrofit2.Response | |
import retrofit2.Retrofit | |
import java.io.IOException | |
class RetrofitException 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 */ | |
val retrofit: Retrofit?) : RuntimeException(message, exception) { | |
/** Identifies the event kind which triggered a [RetrofitException]. */ | |
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 | |
} | |
/** | |
* 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? { | |
val errorBody = response?.errorBody() | |
if (errorBody == null || retrofit == null) { | |
return null | |
} | |
val converter: Converter<ResponseBody, T> = retrofit.responseBodyConverter(type, arrayOfNulls<Annotation>(0)) | |
return converter.convert(errorBody) | |
} | |
companion object { | |
fun httpError(url: String, response: Response<*>, retrofit: Retrofit): RetrofitException { | |
val message = response.code().toString() + " " + response.message() | |
return RetrofitException(message, url, response, Kind.HTTP, null, retrofit) | |
} | |
fun networkError(exception: IOException): RetrofitException { | |
return RetrofitException(exception.message, null, null, Kind.NETWORK, exception, null) | |
} | |
fun unexpectedError(exception: Throwable): RetrofitException { | |
return RetrofitException(exception.message, null, null, Kind.UNEXPECTED, exception, null) | |
} | |
} | |
} |
This file contains hidden or 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
fun createGsonConverterFactory(config: GsonBuilder.() -> Unit): GsonConverterFactory = GsonConverterFactory | |
.create(GsonBuilder() | |
.apply { config() } | |
.create()) | |
/** | |
* Get cache from context for OkHttpClient | |
*/ | |
fun Context.getCache(sizeMb: Int = 10): Cache { | |
val cacheSize = sizeMb * 1024 * 1024 // 10 MiB | |
return Cache(cacheDir, cacheSize.toLong()) | |
} |
This file contains hidden or 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
/** | |
* Users singleton (backed by SingletonHolder) Retrofit instance | |
* and helps to instantiate a Retrofit Service from the interface | |
*/ | |
class RetrofitManager { | |
companion object: SingletonHolder<Context, Retrofit>({ context -> | |
TmdbRetrofitBuilder(context).buildRetrofit() }) { | |
fun <S> createService(context: Context, serviceClass: Class<S>): S = | |
instance(context).create(serviceClass) | |
} | |
} |
This file contains hidden or 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 RxErrorHandlingCallAdapterFactory private constructor() : CallAdapter.Factory() { | |
private val originalFactory by lazy { | |
RxJava2CallAdapterFactory.create() | |
// RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()) | |
} | |
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? { | |
val wrapped = originalFactory.get(returnType, annotations, retrofit) as CallAdapter<out Any, *> | |
return RxCallAdapterWrapper(retrofit, wrapped) | |
} | |
private class RxCallAdapterWrapper<R>(private val retrofit: Retrofit, | |
private val wrappedCallAdapter: CallAdapter<R, *>) : CallAdapter<R, Any> { | |
override fun responseType(): Type { | |
return wrappedCallAdapter.responseType() | |
} | |
@Suppress("UNCHECKED_CAST") | |
override fun adapt(call: Call<R>): Any { | |
val obj = wrappedCallAdapter.adapt(call) | |
return when (obj) { | |
is Flowable<*> -> obj.onErrorResumeNext { throwable: Throwable -> | |
Flowable.error(asRetrofitException(throwable)) | |
} | |
is Single<*> -> obj.onErrorResumeNext { throwable: Throwable -> | |
Single.error(asRetrofitException(throwable)) | |
} | |
is Maybe<*> -> obj.onErrorResumeNext { throwable: Throwable -> | |
Maybe.error(asRetrofitException(throwable)) | |
} | |
is Completable -> obj.onErrorResumeNext { throwable: Throwable -> | |
Completable.error(asRetrofitException(throwable)) | |
} | |
else -> obj | |
} | |
} | |
private fun asRetrofitException(throwable: Throwable): RetrofitException { | |
// We had non-200 http error | |
if (throwable is HttpException) { | |
val response = throwable.response() | |
return RetrofitException.httpError(response.raw().request().url().toString(), response, retrofit) | |
} | |
// A network error happened | |
if (throwable is IOException) { | |
return RetrofitException.networkError(throwable) | |
} | |
// We don't know what happened. We need to simply convert to an unknown error | |
return RetrofitException.unexpectedError(throwable) | |
} | |
} | |
companion object { | |
fun create(): CallAdapter.Factory = RxErrorHandlingCallAdapterFactory() | |
} | |
} |
This file contains hidden or 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
/** | |
* Creation of a singleton that takes an argument. | |
* Normally, using an object declaration in Kotlin you are guaranteed to get a safe and efficient singleton implementation. | |
* But it cannot take extra arguments | |
* https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e | |
* | |
* CppContext everywhere: | |
* https://github.com/LouisCAD/Splitties/tree/master/appctx | |
* | |
* Firebase AppContext trick: | |
* https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html | |
* class Manager private constructor(context: Context) { | |
* init { | |
* // Init using context argument | |
* } | |
* | |
* companion object : SingletonHolder<Manager, Context>(::Manager) | |
* } | |
* | |
* or | |
* class MovieDatabaseHolder { | |
* companion object: SingletonHolder<Application, MovieDao>( | |
* { application -> Room.databaseBuilder(application, MovieDatabase::class.java, "movie-database") | |
* .fallbackToDestructiveMigration().build().movieDao() } | |
* ) | |
* } | |
* Usage example: LocalBroadcastManager.instance(context).sendBroadcast(intent) | |
*/ | |
open class SingletonHolder<in A, out T>(creator: (A) -> T) { | |
private var creator: ((A) -> T)? = creator | |
@Volatile private var instance: T? = null | |
fun instance(arg: A): T { | |
val i = instance | |
if (i != null) { | |
return i | |
} | |
return synchronized(this) { | |
val i2 = instance | |
if (i2 != null) { | |
i2 | |
} else { | |
val created = creator!!(arg) | |
instance = created | |
creator = null | |
created | |
} | |
} | |
} | |
} |
This file contains hidden or 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 YourRetrofitBuilder(private val context: Context): RetrofitBuilder() { | |
override fun Retrofit.Builder.configRetrofit() { | |
baseUrl(us.kostenko.architecturecomponentstmdb.common.api.baseUrl) | |
// addCallAdapterFactory(CoroutineCallAdapterFactory()) | |
addConverterFactory(GsonConverterFactory.create(buildGson())) | |
// client(buildOkHttpClient(context.cacheDir) { cache -> | |
// StethoUtils.getOkHttpClient(cache) { | |
// apply { configOkHttpClient() } | |
// } | |
// }) | |
} | |
override fun OkHttpClient.Builder.configOkHttpClient() { | |
addInterceptor(::headerInterceptor) | |
addInterceptor(::receiveCookieInterceptor) | |
connectTimeout(30, TimeUnit.SECONDS) // connect timeout | |
readTimeout(30, TimeUnit.SECONDS) | |
/** | |
* https://github.com/franmontiel/PersistentCookieJar | |
* http://codezlab.com/add-cookies-interceptor-using-retrofit-android/ | |
* https://gist.github.com/nikhiljha/52d45ca69a8415c6990d2a63f61184ff | |
* https://gist.github.com/tsuharesu/cbfd8f02d46498b01f1b | |
*/ | |
cookieJar(CookieJarManager.instance(context)) | |
} | |
/** | |
* Configure Gson | |
* allows to provide type adapters etc. | |
*/ | |
override fun GsonBuilder.configGson() { | |
// registerTypeAdapter(TicketMenuRoot::class.java, RootItemDeserializer()) | |
setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) | |
} | |
/** | |
* Intercepts requests to add values to the headers | |
*/ | |
private fun headerInterceptor(chain: Interceptor.Chain): Response { | |
val original = chain.request() | |
val request = original.newBuilder() | |
.header("Content-Type", "application/json; charset=utf-8") | |
.header("Accept", "application/json") | |
.header("Accept-Charset", "utf-8") | |
.header("Accept-Language", getLocale()) | |
.method(original.method(), original.body()).build() | |
return chain.proceed(request) | |
} | |
/** | |
* Custom logic to receive cookies | |
*/ | |
private fun receiveCookieInterceptor(chain: Interceptor.Chain): Response { | |
val original = chain.proceed(chain.request()) | |
if (!original.headers("Set-Cookie").isEmpty()) { | |
val cookies = HashSet<String>() | |
for (header in original.headers("Set-Cookie")) { | |
cookies.add(header) | |
} | |
Log.v("OkHttp", "receiveCookieInterceptor, cokies: $cookies") | |
context.saveCookies(cookies) | |
} | |
return original | |
} | |
} | |
/** | |
* Utility function to force use only the two languages | |
*/ | |
fun getLocale() = when(Locale.getDefault().language) { | |
"ru" -> "ru" | |
else -> "en" | |
} | |
/** | |
* Custom cookie handler helper functions | |
*/ | |
const val cPrefCookies = "prefDateCookies" | |
fun Context.saveCookies(cookies: HashSet<String>?) { | |
getPreferences().run { | |
edit() | |
.apply { | |
if (cookies == null) remove(cPrefCookies) | |
else putStringSet(cPrefCookies, cookies) | |
} | |
.apply() | |
} | |
} | |
fun Context.getCookies(): HashSet<String> { | |
return getPreferences() | |
.getStringSet(cPrefCookies, HashSet()) as HashSet | |
} | |
fun Context.getPreferences(): SharedPreferences = | |
PreferenceManager.getDefaultSharedPreferences(this) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment