8000 GitHub - netcosports/Cache
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

netcosports/Cache

Repository files navigation

This is a Kotlin MultiPlatform (KMP) or Android-only library.

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 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.

Table of Contents

  1. KMP projects with Ktor. Android uses OkHttp Engine and iOS uses Darwin Engine
  2. Android-only projects with Retrofit and OkHttp

KMP projects with Ktor. Android uses OkHttp Engine and iOS uses Darwin Engine

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}"
        }
    }

Gradle setup for KMP projects with Ktor

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)
            }
        }
    }
}

Using Ktor directly

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()
}

Using Ktor with OpenApiGenerator

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()
}

Android-only projects with Retrofit and OkHttp

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}"
            }
        }

Gradle setup for Android-only projects

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
}

Manually wrapping your RetrofitService

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)

Generating a RetrofitServiceWrapper

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> with SingleLoader<T>.
  • replaces suspend fun test(): T with fun 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)
0