Cache last version
This is a Kotlin MultiPlatform (KMP) or Android-only library.
In KMP projects we use the Ktor Client with:
- OkHttp Engine and OkHttp Caching feature on Android
- Darwin Engine and NSURLRequest.CachePolicy feature on iOS
The Ktor Client
is wrapped in
a KtorHttpClientWrapper
to create
a CoroutineLoader
instead of responses.
In Android-only projects, we use the OkHttp Client
and OkHttp Caching feature.
You
can create
or generate a RetrofitServiceWrapper
for
the Retrofit Service to
create CoroutineLoader
or SingleLoader
instead of responses.
A CoroutineLoader
/ SingleLoader
can return a cached response, a network response or a
Flow<ResponseWrapper<T>>
/ Observable<ResponseWrapper<T>>
with both responses in the specified
order.
- KMP projects with Ktor. Android uses OkHttp Engine and iOS uses Darwin Engine
- Android-only projects with Retrofit and OkHttp
You can retrieve a cached response, a network response or a Flow<ResponseWrapper<DATA>>
:
val cachedData: DATA = coroutineLoader.cache() //suspend fun
val networkData: DATA = coroutineLoader.api() //suspend fun
val flow: Flow<ResponseWrapper<DATA>> = coroutineLoader.toFlow(MergeArguments.CACHE_AND_API)
.map { responseWrapper ->
if (responseWrapper.isCache) {
"CACHE ${responseWrapper.data}"
} else {
"API ${responseWrapper.data}"
}
}
shared build.gradle
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
// core module
implementation(com.origins-digital.kmp.cache:cache-core:$VERSION)
// coroutine extensions
implementation(com.origins-digital.kmp.cache:cache-core-ktx:$VERSION)
// KtorWrapper
implementation(com.origins-digital.kmp.cache:ktor-cache-data:$VERSION)
}
}
}
}
If you have a Ktor request
:
val client = io.ktor.client.HttpClient()
val repositories: List<Repository> =
client.request("https://api.github.com/repositories").body() //suspend fun
First, wrap your Ktor Client
:
// Both OkHttpClients must have the same Cache file.
private val cacheOkHttpClient = OkHttpClient.Builder()
.cache(
Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
).build()
private val apiOkHttpClient = cacheOkHttpClient.newBuilder()
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
val ktorHttpClientWrapper = KtorHttpClientUtils.createKtorHttpClientWrapper(
okHttpClientDelegate = { isCache ->
/* Configure OkHttpClient depending on whether it's a client for cached responses or network responses */
if (isCache) {
cacheOkHttpClient
} else {
apiOkHttpClient
}
},
configDelegate = { isCache ->
/* Install Ktor Features depending on whether it's a client for cached responses or network responses */
install(ContentNegotiation) {
json(json) // Configure Json for all requests
}
if (!isCache) {
install(Logging) // Enable KtorLogging only for API requests
}
}
)
Then create a CoroutineLoader
for the request:
val coroutineLoader: CoroutineLoader<List<Repository>> = ktorHttpClientWrapper.createLoader {
this/* Ktor HttpClient */.request("https://api.github.com/repositories").body()
}
If you have a generated ExampleApi
:
internal interface ExampleApi {
suspend fun getRepositories(): List<Repository>
companion object {
operator fun invoke(
basePath: String = "http://localhost",
httpClientEngine: HttpClientEngine,
json: Json,
clientBlock: HttpClient.() -> Unit = {},
configBlock: HttpClientConfig<*>.() -> Unit
): ExampleApi = ExampleApiImpl(basePath, httpClientEngine, json, clientBlock, configBlock)
}
}
First, wrap your ExampleApi
:
// Both OkHttpClients must have the same Cache file.
private val cacheOkHttpClient = OkHttpClient.Builder()
.cache(
Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
).build()
private val apiOkHttpClient = cacheOkHttpClient.newBuilder()
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
val ktorApiWrapper = KtorHttpClientUtils.createKtorApiWrapper(
okHttpClientDelegate = { isCache ->
/* Configure OkHttpClient depending on whether it's a client for cached responses or network responses */
if (isCache) {
cacheOkHttpClient
} else {
apiOkHttpClient
}
},
engineDelegate = { httpClientEngine, isCache ->
ExampleApi(
basePath = baseUrl,
httpClientEngine = httpClientEngine,
json = json,
clientBlock = {},
configBlock = {},
)
}
)
Then create a CoroutineLoader
for the request:
val coroutineLoader: CoroutineLoader<List<Repository>> = ktorApiWrapper.createLoader {
this.getRepositories()
}
You can retrieve a cached response, a network response or Flow<ResponseWrapper<DATA>>
/
Observable<ResponseWrapper<DATA>>
:
// Coroutines
val cachedData: DATA = coroutineLoader.cache() //suspend fun
val networkData: DATA = coroutineLoader.api() //suspend fun
val flow: Flow<ResponseWrapper<DATA>> = coroutineLoader.toFlow(MergeArguments.CACHE_AND_API)
.map { responseWrapper ->
if (responseWrapper.isCache) {
"CACHE ${responseWrapper.data}"
} else {
"API ${responseWrapper.data}"
}
}
// RX
val cachedData: Single<DATA> = singleLoader.cache()
val networkData: Single<DATA> = singleLoader.api()
val observable: Observable<ResponseWrapper<DATA>> =
singleLoader.toObservable(MergeArguments.CACHE_AND_API)
.map { responseWrapper ->
if (responseWrapper.isCache) {
"CACHE ${responseWrapper.data}"
} else {
"API ${responseWrapper.data}"
}
}
plugins {
kotlin("kapt") // add this plugin to gerenerate a RetrofitServiceWrapper for your Retrofit Service
}
dependencies {
// required
implementation(project(Config.Deps.LibsModules.cacheCore)) // core, jvm module
// required to configure cache in the OkHttpClient
implementation(project(Config.Deps.LibsModules.okhttpCacheData)) // okhttp utls, jvm module
// You can add BOTH if you use suspend requests but have some legacy Rx requests
implementation(project(Config.Deps.LibsModules.cacheCoreKtx)) // coroutines, jvm module
implementation(project(Config.Deps.LibsModules.cacheCoreRx)) // Rx, jvm module
// add these modules to gerenerate a RetrofitServiceWrapper for your Retrofit Service
implementation(project(Config.Deps.LibsModules.retrofitCacheData)) // retrofit utils, jvm module
kapt(project(Config.Deps.LibsModules.retrofitCacheProcessor)) // retrofit kapt processor, jvm module
}
For example, if you have a Retrofit Service
:
interface SampleRetrofitService {
@GET
fun getResponseSingle(@Url url: String): Single<List<Any>>
@GET
fun getResponseObservable(@Url url: String): Observable<List<Any>>
@GET
suspend fun getResponseSuspend(@Url url: String): List<Any>
}
You can copy this small class and modify it if you want:
class RetrofitServiceWrapper<SERVICE>(
private val retrofitDelegate: (isCache: Boolean) -> SERVICE,
) {
val cacheService: SERVICE by lazy { retrofitDelegate(/*isCache = */true) }
val apiService: SERVICE by lazy { retrofitDelegate(/*isCache = */false) }
fun <RESULT : Any> coroutine(delegate: suspend SERVICE.() -> RESULT): CoroutineLoader<RESULT> {
return suspendLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> delegate(apiService)
is LoaderArguments.CACHE -> delegate(cacheService)
}
}
}
fun <RESULT : Any> single(delegate: SERVICE.() -> Single<RESULT>): SingleLoader<RESULT> {
return singleLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> delegate(apiService)
is LoaderArguments.CACHE -> delegate(cacheService)
}
}
}
}
Then wrap your real RetrofitService
:
// Both OkHttpClients must have the same Cache file.
private val cacheOkHttpClient = OkHttpClient.Builder()
.cache(
Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
).build()
private val apiOkHttpClient = cacheOkHttpClient.newBuilder()
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
val gson = GsonBuilder().setLenient().create()
val retrofitServiceWrapper = RetrofitServiceWrapper<SampleRetrofitService>(
retrofitDelegate = { isCache ->
val retrofitBuilder = Retrofit.Builder()
.baseUrl("https://baseurl.com/")
.client(
if (isCache) {
cacheOkHttpClient
} else {
apiOkHttpClient
}
)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
.create(SampleRetrofitService::class.java)
}
)
Get your CoroutineLoader
, SingleLoader
or unmodified requests:
val coroutineLoader: CoroutineLoader<List<Any>> =
retrofitServiceWrapper.coroutine { this/*SampleRetrofitService*/.getResponseSuspend(url = url) }
val singleLoader: SingleLoader<List<Any>> =
retrofitServiceWrapper.single { this/*SampleRetrofitService*/.getResponseSingle(url = url) }
val observable: Observable<List<Any>> =
retrofitServiceWrapper.apiService.getResponseObservable(url = url)
For example, if you have a Retrofit Service
:
interface SampleRetrofitService {
@GET
fun getResponseSingle(@Url url: String): Single<List<Any>>
@GET
fun getResponseObservable(@Url url: String): Observable<List<Any>>
@GET
suspend fun getResponseSuspend(@Url url: String): List<Any>
}
First, annotate it with @CacheService
:
@CacheService
interface SampleRetrofitService {
/* code */
}
Run assemble
or kapt{BuildType}Kotlin
task for this module to generate a
RetrofitServiceWrapper
.
Ensure the kapt plugin and processor are added to your module.
Once the task is completed, the RetrofitServiceWrapper
will be generated (in our example it will
be
SampleRetrofitServiceWrapper
)
The RetrofitServiceWrapper
:
- replaces
Single<T>
withSingleLoader<T>
. - replaces
suspend fun test(): T
withfun test(): CoroutineLoader<T>
. - Doesn't modify functions returning other types.
public class SampleRetrofitServiceWrapper(
public val cacheService: SampleRetrofitService,
public val apiService: SampleRetrofitService,
) {
public fun getResponseSuspend(url: String): CoroutineLoader<List<Any>> {
return suspendLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> apiService.getResponseSuspend(url)
is LoaderArguments.CACHE -> cacheService.getResponseSuspend(url)
}
}
}
public fun getResponseSingle(url: String): SingleLoader<List<Any>> {
return singleLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> apiService.getResponseSingle(url)
is LoaderArguments.CACHE -> cacheService.getResponseSingle(url)
}
}
}
public fun getResponseObservable(url: String): Observable<List<Any>> {
return apiService.getResponseObservable(url)
}
public companion object {
public fun create(
retrofitBuilder: Retrofit.Builder,
okHttpClient: OkHttpClient,
maxStale: Long = DEFAULT_MAX_STALE,
): SampleRetrofitServiceWrapper {
return createServiceWrapper<SampleRetrofitService, SampleRetrofitServiceWrapper>(
retrofitBuilder = retrofitBuilder,
okHttpClient = okHttpClient,
maxStale = maxStale,
) { cacheService, apiService ->
SampleRetrofitServiceWrapper(cacheService, apiService)
}
}
}
}
Initialize the SampleRetrofitServiceWrapper
:
// Both OkHttpClients must have the same Cache file.
private val cacheOkHttpClient = OkHttpClient.Builder()
.cache(
Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
).build()
private val apiOkHttpClient = cacheOkHttpClient.newBuilder()
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
val gson = GsonBuilder().setLenient().create()
// just provide the Retrofit.Builder with apiOkHttpClient and get the Wrapper
val retrofitBuilder = Retrofit.Builder()
.baseUrl("https://baseurl.com/")
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
// provide apiOkHttpClient, the CacheOkHttpClient will be created by making a copy without interceptors
return SampleRetrofitServiceWrapper.create(retrofitBuilder, apiOkHttpClient)
// or create the Wrapper yourself if you need different interceptors for cache and api and etc
val cacheService = Retrofit.Builder()
.baseUrl("https://baseurl.com/")
.client(
OkHttpClientUtils.setupCache(
okHttpClient = cacheOkHttpClient,
onlyCache = true
)
)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
.create(SampleRetrofitService::class.java)
val apiService = Retrofit.Builder()
.baseUrl("https://baseurl.com/")
.client(
OkHttpClientUtils.setupCache(
okHttpClient = apiOkHttpClient,
onlyCache = false
)
)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
.create(SampleRetrofitService::class.java)
return SampleRetrofitServiceWrapper(cacheService, apiService)
Get your CoroutineLoader
, SingleLoader
or unmodified requests:
val coroutineLoader: CoroutineLoader<List<Any>> =
retrofitServiceWrapper.getResponseSuspend(url = url)
val singleLoader: SingleLoader<List<Any>> = retrofitServiceWrapper.getResponseSingle(url = url)
val observable: Observable<List<Any>> = retrofitServiceWrapper.getResponseObservable(url = url)