diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b425cbc2..a7a2c88f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -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 @@ -107,8 +109,7 @@ kotlin { val desktopAndroidMain by getting { dependencies { - // Okhttp engine - implementation(libs.ktor.okhttp) + } } diff --git a/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidApplicationComponent.kt b/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidApplicationComponent.kt index 50b72434..ca0b8e8c 100644 --- a/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidApplicationComponent.kt +++ b/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidApplicationComponent.kt @@ -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 @@ -15,5 +16,7 @@ abstract class AndroidApplicationComponent( override val dataStoreParentDir: Path = applicationContext.filesDir.toOkioPath() + override val okhttpCacheParentDir: File = applicationContext.cacheDir + companion object } diff --git a/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.kt b/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.kt index e737ca4e..4ab972f3 100644 --- a/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.kt +++ b/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.kt @@ -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 @@ -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")) { @@ -45,7 +88,20 @@ 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) @@ -53,5 +109,3 @@ interface NetworkComponent { } } } - -expect fun getEngineFactory(): HttpClientEngineFactory<*> diff --git a/shared/src/desktopAndroidMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.shared.kt b/shared/src/desktopAndroidMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.shared.kt deleted file mode 100644 index f4462ae0..00000000 --- a/shared/src/desktopAndroidMain/kotlin/com/mr3y/ludi/shared/di/NetworkComponent.shared.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mr3y.ludi.shared.di - -import io.ktor.client.engine.HttpClientEngineFactory -import io.ktor.client.engine.okhttp.OkHttp - -actual fun getEngineFactory(): HttpClientEngineFactory<*> = OkHttp diff --git a/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppCacheDir.kt b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppCacheDir.kt new file mode 100644 index 00000000..6003dce9 --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppCacheDir.kt @@ -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") +} diff --git a/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopApplicationComponent.kt b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopApplicationComponent.kt index b10a8f20..4735daf4 100644 --- a/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopApplicationComponent.kt +++ b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopApplicationComponent.kt @@ -2,9 +2,11 @@ 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 @@ -12,5 +14,7 @@ abstract class DesktopApplicationComponent : SharedApplicationComponent, Desktop override val dataStoreParentDir: Path = getAppDir().toOkioPath() + override val okhttpCacheParentDir: File = getCacheDir() + companion object } diff --git a/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/ImageLoader.kt b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/ImageLoader.kt index 90f718f6..c4c4102f 100644 --- a/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/ImageLoader.kt +++ b/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/ImageLoader.kt @@ -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 { @@ -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") -}