Skip to content

Commit

Permalink
Add Network layer Caching by leveraging Okhttp's Caching (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
mr3y-the-programmer authored Oct 23, 2023
1 parent 1029f27 commit f909748
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 25 deletions.
5 changes: 3 additions & 2 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ kotlin {
implementation(libs.ktor.core)
implementation(libs.ktor.content.negotation)
implementation(libs.ktor.kotlinx.serialization)
// Okhttp engine
implementation(libs.ktor.okhttp)
implementation(libs.kotlinx.serialization)

// Paging
Expand Down Expand Up @@ -107,8 +109,7 @@ kotlin {

val desktopAndroidMain by getting {
dependencies {
// Okhttp engine
implementation(libs.ktor.okhttp)

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import okio.Path
import okio.Path.Companion.toOkioPath
import java.io.File

@Component
@Singleton
Expand All @@ -15,5 +16,7 @@ abstract class AndroidApplicationComponent(

override val dataStoreParentDir: Path = applicationContext.filesDir.toOkioPath()

override val okhttpCacheParentDir: File = applicationContext.cacheDir

companion object
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,67 @@
package com.mr3y.ludi.shared.di

import com.mr3y.ludi.shared.BuildConfig
import com.mr3y.ludi.shared.core.Logger
import com.mr3y.ludi.shared.core.network.rssparser.Parser
import com.mr3y.ludi.shared.core.network.rssparser.internal.DefaultParser
import com.mr3y.ludi.shared.di.annotations.Singleton
import com.prof18.rssparser.RssParser
import com.prof18.rssparser.RssParserBuilder
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.api.createClientPlugin
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Provides
import okhttp3.Cache
import okhttp3.Call
import okhttp3.EventListener
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.File
import java.io.IOException

interface NetworkComponent {

@get:Provides
val okhttpCacheParentDir: File

@Singleton
@Provides
fun provideThirdPartyRssParserInstance(): RssParser {
return RssParser()
fun provideOkhttpClientInstance(okhttpCacheParentDir: File, logger: Logger): OkHttpClient {
return OkHttpClient.Builder()
.cache(Cache(directory = File(okhttpCacheParentDir, "okhttp_cache"), maxSize = 80L * 1024L * 1024L))
.eventListener(object : EventListener() {
override fun callStart(call: Call) {
logger.v { "Started Executing call: $call" }
}

override fun cacheHit(call: Call, response: Response) {
logger.d { "Cache Hit for this request: $call! Retrieving response from cache, cached response is : $response." }
}

override fun cacheMiss(call: Call) {
logger.d { "Cache Miss for this request: $call! Retrieving response from network" }
}

override fun cacheConditionalHit(call: Call, cachedResponse: Response) {
logger.d { "Checking if cached $cachedResponse isn't stale for request: $call." }
}

override fun callFailed(call: Call, ioe: IOException) {
logger.w { "Failed to execute call: $call, exception: $ioe" }
}
})
.build()
}

@Singleton
@Provides
fun provideThirdPartyRssParserInstance(okhttpClient: OkHttpClient): RssParser {
return RssParserBuilder(callFactory = okhttpClient).build()
}

@Singleton
Expand All @@ -35,7 +78,7 @@ interface NetworkComponent {

@Provides
@Singleton
fun provideKtorClientInstance(jsonInstance: Json): HttpClient {
fun provideKtorClientInstance(okhttpClient: OkHttpClient, jsonInstance: Json): HttpClient {
val rawgApiInterceptorPlugin = createClientPlugin("RAWGAPIKeyInterceptor") {
onRequest { request, _ ->
if (request.url.toString().contains("api.rawg.io")) {
Expand All @@ -45,13 +88,24 @@ interface NetworkComponent {
}
}
}
return HttpClient(getEngineFactory()) {
return HttpClient(OkHttp) {
engine {
preconfigured = okhttpClient
}
install(HttpRequestRetry) {
retryIf(3) { _, httpResponse ->
when {
httpResponse.status.value in 500..599 -> true
httpResponse.status == HttpStatusCode.TooManyRequests -> true
else -> false
}
}
constantDelay()
}
install(rawgApiInterceptorPlugin)
install(ContentNegotiation) {
json(jsonInstance)
}
}
}
}

expect fun getEngineFactory(): HttpClientEngineFactory<*>

This file was deleted.

10 changes: 10 additions & 0 deletions shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppCacheDir.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mr3y.ludi.shared

import java.io.File

fun getCacheDir() = when (currentOperatingSystem) {
OperatingSystem.Windows -> File(System.getenv("AppData"), "Ludi/cache")
OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/Ludi")
OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/Ludi")
else -> throw IllegalStateException("Unsupported operating system")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package com.mr3y.ludi.shared.di

import com.mr3y.ludi.shared.di.annotations.Singleton
import com.mr3y.ludi.shared.getAppDir
import com.mr3y.ludi.shared.getCacheDir
import me.tatarka.inject.annotations.Component
import okio.Path
import okio.Path.Companion.toOkioPath
import java.io.File

@Component
@Singleton
abstract class DesktopApplicationComponent : SharedApplicationComponent, DesktopCrashReportingComponent {

override val dataStoreParentDir: Path = getAppDir().toOkioPath()

override val okhttpCacheParentDir: File = getCacheDir()

companion object
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.mr3y.ludi.shared.ui.components

import com.mr3y.ludi.shared.OperatingSystem
import com.mr3y.ludi.shared.currentOperatingSystem
import com.mr3y.ludi.shared.getCacheDir
import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.component.setupDefaultComponents
import com.seiko.imageloader.defaultImageResultMemoryCache
import okio.Path.Companion.toOkioPath
import java.io.File

internal fun generateImageLoader(): ImageLoader {
return ImageLoader {
Expand All @@ -26,10 +24,3 @@ internal fun generateImageLoader(): ImageLoader {
}
}
}

private fun getCacheDir() = when (currentOperatingSystem) {
OperatingSystem.Windows -> File(System.getenv("AppData"), "Ludi/cache")
OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/Ludi")
OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/Ludi")
else -> throw IllegalStateException("Unsupported operating system")
}

0 comments on commit f909748

Please sign in to comment.