Client cache & Retrofit

Known approach (cache-control)

Wrapping Api interface

interface RocketApi {
fun search(@Query("filter") filter: String): Single<SearchResult>

fun getById(@Query("rocket_id") id: String): Single<Rocket>

class CacheRocketApi(
//provide DB, repository, file or any other local storage dependency
private val origin: RocketApi
) : RocketApi {
override fun search(filter: String) =, 15 * SECOND)

override fun getById(id: String) = origin.getById(id).wrapWithCaching(HOUR, 5 * MINUTE)

private fun <T> Single<T>.wrapWithCaching(expirationTimeMs: Long, staleTimeMs: Long): Single<T> {
//provide your cache strategy here

companion object {
private const val SECOND = 1000L
private const val MINUTE = 60 * SECOND
private const val HOUR
= 60 * MINUTE

class SearchResult
class Rocket
interface WitcherApi {
@WithCache(stalesAfterMs = 15 * SECOND, expiresAfterMs = HOUR)
fun searchBeast(@Query("smell") smell: String): Single<Beast>

@WithCache(stalesAfterMs = 5 * MINUTE, expiresAfterMs = HOUR)
fun getPosionById(@Query("posion_id") id: String): Single<Posion>

Client-side approach


Providing a Cache

interface CacheProvider {
fun obtainFresh(key: String): String?
fun obtain(key: String): String?
fun store(key: String, value: String, stalesAfterMs: Long, expiresAfterMs: Long)
private fun generateKey(method: Method, args: Array<out Any>) =
with(method) { (declaringClass.canonicalName ?: "") + "." + name } +
args.joinToString(prefix = "_", separator = "_") { it.javaClass.canonicalName + "&" + it.toString() }
interface WitcherApi {
@WithCache(stalesAfterMs = 15 * SECOND, expiresAfterMs = HOUR)
fun searchBeast(@Query("smell") smell: String): Single<Beast>

fun searchBeastWithoutCache(@Query("smell") smell: String): Single<Beast>

Providing a Network

interface NetProvider {
val isOnline: Boolean
class ContextNetProvider(
private val context: Context
) : NetProvider {
override val isOnline: Boolean get() = context.isOnline()
fun Context.isOnline() = connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true

Context.connectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

Implementation for Single

val interfaceCls =
wrapper.wrap(interfaceCls, retrofit.create(interfaceCls))
interface Serializer {
fun serialize(obj: Any): String
fun deserialize(plain: String, objType: Type): Any


  • We forced to use Gson, for caching and restoring objects. As I mentioned earlier, we can eliminate that by introducing high level interface Serializer and make implementation depending on serialization library: Gson, Jackson and e.t.c
  • We have to make @WithCache annotated request return Single<T>. I will stop your right here and say, that if you want to see generalization of wrap method for not only single, but also for Observable and Flowable, then please go to the BONUS section.
  • We have to configure Retrofit in the way of using Rx. If we will use courutines or just Calls, then we will need to provide a wrap method for them. I think we can do that, but for me and for now it is just fine, that we working only with Rx.


Bonus: RxTemplate

internal interface RxTemplate<Actual, T> {
val actual: Actual
fun doOnNext(onNext: (T) -> Unit): RxTemplate<Actual, T>
fun onErrorResumeNext(onResumeNext: (Throwable, RxTemplateFactory<Actual, T>) -> RxTemplate<Actual, T>): RxTemplate<Actual, T>
internal interface RxTemplateFactory<Actual, T> {
fun just(item: T): RxTemplate<Actual, T>
fun error(thr: Throwable): RxTemplate<Actual, T>
fun defer(supplier: (RxTemplateFactory<Actual, T>) -> RxTemplate<Actual, T>): RxTemplate<Actual, T>
fun create(actual: Actual): RxTemplate<Actual, T>



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store