oligazar
4/8/2018 - 11:28 AM

Retrofit

  1. RetrofitException
  2. RxErrorHandlingCallAdapterFactory
  3. YourRetrofitBuilder
  4. RetrofitBuilder
  5. RetrofitManager
  6. CookieManager
  7. SingletonHolder
  8. Injector
  9. RetrofitHelpers
/**
 * Handles cookies for retrofit requests
 *
 * 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
 */
class CookieJarManager {
    companion object: SingletonHolder<Context, PersistentCookieJar>(
            { context -> PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context)) }
    )
}
/**
 * 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)
    }
}
/**
 * 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()
}
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)
        }
    }
}
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())
}
/**
 * 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)
    }
}
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()
    }
}
/**
 * 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
            }
        }
    }
}
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)