Cache
3.0.1indexedLibrary facilitates efficient HTTP response caching, utilizing Ktor and OkHttp clients with features like CoroutineLoader and SingleLoader to manage network and cached data responses seamlessly.
Library facilitates efficient HTTP response caching, utilizing Ktor and OkHttp clients with features like CoroutineLoader and SingleLoader to manage network and cached data responses seamlessly.
Cache is a lightweight Kotlin MultiPlatform (KMP) or Android-only library that makes Ktor ( KMM) and OkHttp (Android) networking and caching easy. Library provides a simple API for requesting the cache, the network or both in the specified order. Also it provides a simple API for using your current Databases in caching requests.
In KMP projects we use the Ktor Client with:
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 for
the to
create
or
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.
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-ktx:$VERSION)
// KtorWrapper
implementation(com.origins-digital.kmp.cache:cache-ktor-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:
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 : List<Repository>
{
: ExampleApi = ExampleApiImpl(basePath, httpClientEngine, json, clientBlock, configBlock)
}
}
First, wrap your ExampleApi:
Then create a CoroutineLoader for the request:
val coroutineLoader: CoroutineLoader<List<Repository>> = ktorApiWrapper.createLoader {
this.getRepositories()
}
KMM 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-ktx:$VERSION)
}
}
}
}
or
Android build.gradle
dependencies {
implementation(com.origins-digital.kmp.cache:cache-core:$VERSION) // core, kmm module
// You can add BOTH if you use suspend requests but have some legacy Rx requests
implementation(com.origins-digital.kmp.cache:cache-ktx:$VERSION) // coroutines, kmm module
implementation(com.origins-digital.kmp.cache:cache-rx:$VERSION) // Rx, jvm module
}
For example, if you have a SampleKtorApiDataSource and SampleDatabaseDataSource:
You can create CoroutineLoader/SingleLoader:
Now you can retrieve a cached response, a network response or Flow<ResponseWrapper<DATA>>/
Observable<ResponseWrapper<DATA>>:
You can retrieve a cached response, a network response or Flow<ResponseWrapper<DATA>>/
Observable<ResponseWrapper<DATA>>:
plugins {
// add this plugin to GENERATE a RetrofitServiceWrapper for your Retrofit Service
kotlin("kapt")
}
dependencies {
// required core module
implementation(com.origins-digital.kmp.cache:cache-core:$VERSION) // core, kmm module
// required to configure cache in the OkHttpClient
implementation(com.origins-digital.kmp.cache:cache-okhttp-data:$VERSION) // okhttp utls, jvm module
// You can add BOTH if you use suspend requests but have some legacy Rx requests
implementation(com.origins-digital.kmp.cache:cache-ktx:$VERSION) // coroutines, kmm module
implementation(com.origins-digital.kmp.cache:cache-rx:$VERSION) // Rx, jvm module
// add these modules to GENERATE a RetrofitServiceWrapper for your Retrofit Service
implementation(com.origins-digital.kmp.cache:cache-retrofit-data:$VERSION) // annotation for processor, jvm module
implementation(com.origins-digital.kmp.cache:cache-retrofit-processor:$VERSION) // kapt processor, jvm module
}
For example, if you have a Retrofit Service:
interface SampleRetrofitService {
@GET
fun getResponseSingle(@Url url: String): Single<List<Any>>
: Observable<List<Any>>
: List<Any>
}
You can copy this small class and modify it if you want:
Then wrap your real RetrofitService:
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>>
: Observable<List<Any>>
: 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:
Single<T> with SingleLoader<T>.suspend fun test(): T with fun test(): CoroutineLoader<T>.Initialize the SampleRetrofitServiceWrapper:
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)
RetrofitServiceWrapper// Both OkHttpClients must have the same Cache file.
private val cache = Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
private val baseOkHttpClient = OkHttpClient.Builder().build()
private val cacheOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = true,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.build()
private val apiOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = false,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.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
}
}
)
// Both OkHttpClients must have the same Cache file.
private val cache = Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
private val baseOkHttpClient = OkHttpClient.Builder().build()
private val cacheOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = true,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.build()
private val apiOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = false,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.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 = {},
)
}
)
class SampleKtorApiDataSource {
suspend fun getRepositoriesCoroutine(): List<Any> {
return ktor.get("https://api.github.com/repositories").body()
}
fun getRepositoriesSingle(): Single<List<Any>> {
return retrofit.get("https://api.github.com/repositories")
}
}
class SampleDatabaseDataSource {
suspend fun getRepositoriesCoroutine(): List<Any> {
return database.getRepositoriesCoroutine()
}
fun getRepositoriesSingle(): Single<List<Any>> {
return database.getRepositoriesSingle()
}
}
class SampleRepositoryImpl(
private val apiDataSource: SampleKtorApiDataSource,
private val databaseDataSource: SampleDatabaseDataSource,
) : SampleRepository {
override fun getRepositoriesCoroutine(): CoroutineLoader<List<Any>> {
return suspendLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> apiDataSource.getRepositoriesCoroutine()
is LoaderArguments.CACHE -> databaseDataSource.getRepositoriesCoroutine()
}
}
}
override fun getRepositoriesSingle(): SingleLoader<List<Any>> {
return singleLoader { loaderArguments ->
when (loaderArguments) {
is LoaderArguments.API -> apiDataSource.getRepositoriesSingle()
is LoaderArguments.CACHE -> databaseDataSource.getRepositoriesSingle()
}
}
}
}
val sampleRepository: SampleRepository = TODO()
// Coroutines
val coroutineLoader = sampleRepository.getRepositoriesCoroutine()
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 singleLoader = sampleRepository.getRepositoriesSingle()
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}"
}
}
// 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}"
}
}
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)
}
}
}
}
// Both OkHttpClients must have the same Cache file.
private val cache = Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
private val baseOkHttpClient = OkHttpClient.Builder().build()
private val cacheOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = true,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.build()
private val apiOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = false,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
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)
}
)
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)
}
}
// Both OkHttpClients must have the same Cache file.
private val cache = Cache(
directory = File(app.cacheDir, "cache"),
maxSize = 10L * 1024 * 1024, // 10 MiB
)
private val baseOkHttpClient = OkHttpClient.Builder().build()
private val cacheOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = true,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.build()
private val apiOkHttpClient = baseOkHttpClient.newBuilder()
.setupCache(
cache = cache,
onlyCache = false,
maxStale = OkHttpCacheInterceptor.DEFAULT_MAX_STALE
)
.addInterceptor(ChuckerInterceptor(app)) // Configure Chucker for API requests only
.build()
val createRetrofitClient: (okHttpClient: OkHttpClient) -> SampleRetrofitService = { okHttpClient ->
Retrofit.Builder()
.baseUrl("https://api.curator.io/v1/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
.create(SampleRetrofitService::class.java)
}
return SampleRetrofitServiceWrapper(
createRetrofitClient(cacheOkHttpClient),
createRetrofitClient(apiOkHttpClient)
)
Surfaced from shared tags and platforms — no rankings paid for.