diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 520baa134..11f802f3c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -97,7 +97,7 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller)
implementation(libs.kotlinx.coroutines.guava)
- implementation(libs.coil.kt)
+ implementation(libs.coil)
debugImplementation(libs.androidx.compose.ui.testManifest)
debugImplementation(projects.uiTestHiltManifest)
diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts
index d68117d06..5d0539ce3 100644
--- a/core/designsystem/build.gradle.kts
+++ b/core/designsystem/build.gradle.kts
@@ -40,7 +40,7 @@ dependencies {
debugApi(libs.androidx.compose.ui.tooling)
- implementation(libs.coil.kt.compose)
+ implementation(libs.coil.compose)
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.accompanist.testharness)
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 8edda61fe..80650b276 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -46,8 +46,10 @@ kotlin {
api(libs.kotlinx.datetime)
api(projects.core.common)
api(projects.core.model)
- implementation(libs.coil.kt)
- implementation(libs.coil.kt.svg)
+ implementation(libs.coil)
+ implementation(libs.coil.core)
+ implementation(libs.coil.svg)
+ implementation(libs.coil.network.ktor)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.json)
@@ -56,9 +58,9 @@ kotlin {
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktorfit.lib)
- implementation(libs.ktorfit.ksp)
}
commonTest.dependencies {
+ implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
androidMain.dependencies {
@@ -67,9 +69,9 @@ kotlin {
appleMain.dependencies {
implementation(libs.ktor.client.darwin)
}
- jsMain.dependencies {
- implementation(libs.ktor.client.js)
- }
+// wasmJsMain.dependencies {
+// implementation(libs.ktor.client.js)
+// }
jvmMain.dependencies {
implementation(libs.ktor.client.java)
}
@@ -81,4 +83,12 @@ kotlin {
dependencies {
add("kspCommonMainMetadata", libs.ktorfit.ksp)
+ add("kspAndroid", libs.ktorfit.ksp)
+// add("kspWasmJs", libs.ktorfit.ksp)
+ add("kspJvm", libs.ktorfit.ksp)
+ add("kspIosX64", libs.ktorfit.ksp)
+ add("kspIosArm64", libs.ktorfit.ksp)
+ add("kspIosSimulatorArm64", libs.ktorfit.ksp)
+ add("kspMacosX64", libs.ktorfit.ksp)
+ add("kspMacosArm64", libs.ktorfit.ksp)
}
diff --git a/core/network/src/commonMain/kotlin/JvmUnitTestFakeAssetManager.kt b/core/network/src/commonMain/kotlin/JvmUnitTestFakeAssetManager.kt
deleted file mode 100644
index 79370d5a8..000000000
--- a/core/network/src/commonMain/kotlin/JvmUnitTestFakeAssetManager.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import androidx.annotation.VisibleForTesting
-import com.google.samples.apps.nowinandroid.core.network.fake.FakeAssetManager
-import java.io.File
-import java.io.InputStream
-import java.util.Properties
-
-/**
- * This class helps with loading Android `/assets` files, especially when running JVM unit tests.
- * It must remain on the root package for an easier [Class.getResource] with relative paths.
- * @see UnitTestOptions
- */
-@VisibleForTesting
-internal object JvmUnitTestFakeAssetManager : FakeAssetManager {
- private val config =
- requireNotNull(javaClass.getResource("com/android/tools/test_config.properties")) {
- """
- Missing Android resources properties file.
- Did you forget to enable the feature in the gradle build file?
- android.testOptions.unitTests.isIncludeAndroidResources = true
- """.trimIndent()
- }
- private val properties = Properties().apply { config.openStream().use(::load) }
- private val assets = File(properties["android_merged_assets"].toString())
-
- override fun open(fileName: String): InputStream = File(assets, fileName).inputStream()
-}
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DiskCacheComponent.kt
similarity index 67%
rename from core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt
rename to core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DiskCacheComponent.kt
index 53ad7d48d..e954315fd 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DiskCacheComponent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package com.google.samples.apps.nowinandroid.core.network.fake
+package com.google.samples.apps.nowinandroid.core.network.di
-import java.io.InputStream
+import coil3.disk.DiskCache
+import me.tatarka.inject.annotations.Provides
-fun interface FakeAssetManager {
- fun open(fileName: String): InputStream
-}
+expect class DiskCacheComponent {
+ @Provides
+ internal fun newDiskCache(): DiskCache?
+}
\ No newline at end of file
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt
index 60744eeab..a6aff8b95 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt
@@ -18,15 +18,8 @@ package com.google.samples.apps.nowinandroid.core.network.di
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-@Module
-@InstallIn(SingletonComponent::class)
internal interface FlavoredNetworkModule {
- @Binds
fun binds(impl: FakeNiaNetworkDataSource): NiaNetworkDataSource
}
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/ImageLoaderComponent.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/ImageLoaderComponent.kt
new file mode 100644
index 000000000..cfae7527f
--- /dev/null
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/ImageLoaderComponent.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.network.di
+
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.disk.DiskCache
+import coil3.memory.MemoryCache
+import coil3.request.crossfade
+import coil3.util.DebugLogger
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+
+@Component
+abstract class ImageLoaderComponent {
+ /**
+ * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
+ * format. During Coil's initialization it will call `applicationContext.newImageLoader()` to
+ * obtain an ImageLoader.
+ *
+ * @see Coil
+ */
+ @Provides
+ fun provideImageLoader(
+ context: PlatformContext,
+ diskCache: DiskCache?,
+ debug: Boolean,
+ ): ImageLoader {
+ return ImageLoader.Builder(context)
+ .memoryCache {
+ MemoryCache.Builder()
+ // Set the max size to 25% of the app's available memory.
+ .maxSizePercent(context, percent = 0.25)
+ .build()
+ }
+ .diskCache {
+ diskCache
+ }
+ // Show a short crossfade when loading images asynchronously.
+ .crossfade(true)
+ // Enable logging if this is a debug build.
+ .apply {
+ if (debug) {
+ logger(DebugLogger())
+ }
+ }
+ .build()
+ }
+}
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt
index 7a54b0fc9..4408885c5 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt
@@ -16,61 +16,38 @@
package com.google.samples.apps.nowinandroid.core.network.di
-import com.google.samples.apps.nowinandroid.core.network.fake.FakeAssetManager
-import de.jensklingenberg.ktorfit.Call
+import de.jensklingenberg.ktorfit.Ktorfit
+import de.jensklingenberg.ktorfit.converter.builtin.CallConverterFactory
+import de.jensklingenberg.ktorfit.converter.builtin.FlowConverterFactory
+import de.jensklingenberg.ktorfit.ktorfit
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
+import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
-internal object NetworkModule {
+@Component
+internal abstract class NetworkModule {
+ @Provides
fun providesNetworkJson(): Json = Json {
ignoreUnknownKeys = true
}
- fun providesFakeAssetManager(
- @ApplicationContext context: Context,
- ): FakeAssetManager = FakeAssetManager(context.assets::open)
-
- @Provides
- @Singleton
- fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") {
- OkHttpClient.Builder()
- .addInterceptor(
- HttpLoggingInterceptor()
- .apply {
- if (BuildConfig.DEBUG) {
- setLevel(HttpLoggingInterceptor.Level.BODY)
- }
- },
- )
- .build()
- }
-
- /**
- * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
- * format. During Coil's initialization it will call `applicationContext.newImageLoader()` to
- * obtain an ImageLoader.
- *
- * @see Coil
- */
@Provides
- @Singleton
- fun imageLoader(
- // We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
- okHttpCallFactory: dagger.Lazy,
- @ApplicationContext application: Context,
- ): ImageLoader = trace("NiaImageLoader") {
- ImageLoader.Builder(application)
- .callFactory { okHttpCallFactory.get() }
- .components { add(SvgDecoder.Factory()) }
- // Assume most content images are versioned urls
- // but some problematic images are fetching each time
- .respectCacheHeaders(false)
- .apply {
- if (BuildConfig.DEBUG) {
- logger(DebugLogger())
+ fun provideKtorfit(json: Json): Ktorfit = ktorfit {
+ baseUrl(BuildConfig.BACKEND_URL)
+ httpClient(
+ HttpClient {
+ install(ContentNegotiation) {
+ json(json)
}
- }
- .build()
+ },
+ )
+ converterFactories(
+ FlowConverterFactory(),
+ CallConverterFactory(),
+ )
}
}
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt
index 6ef90ecff..8b4b235ff 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt
@@ -16,39 +16,34 @@
package com.google.samples.apps.nowinandroid.core.network.fake
-import JvmUnitTestFakeAssetManager
-import com.google.samples.apps.nowinandroid.core.network.Dispatcher
-import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
+import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.decodeFromStream
-import javax.inject.Inject
+import me.tatarka.inject.annotations.Inject
/**
* [NiaNetworkDataSource] implementation that provides static news resources to aid development
*/
class FakeNiaNetworkDataSource @Inject constructor(
- @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
+ private val ioDispatcher: IODispatcher,
private val networkJson: Json,
- private val assets: FakeAssetManager = JvmUnitTestFakeAssetManager,
) : NiaNetworkDataSource {
@OptIn(ExperimentalSerializationApi::class)
override suspend fun getTopics(ids: List?): List =
withContext(ioDispatcher) {
- assets.open(TOPICS_ASSET).use(networkJson::decodeFromStream)
+ networkJson.decodeFromString(TOPICS_ASSET)
}
@OptIn(ExperimentalSerializationApi::class)
override suspend fun getNewsResources(ids: List?): List =
withContext(ioDispatcher) {
- assets.open(NEWS_ASSET).use(networkJson::decodeFromStream)
+ networkJson.decodeFromString(NEWS_ASSET)
}
override suspend fun getTopicChangeList(after: Int?): List =
diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt
index e9fe99d9e..cac997563 100644
--- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt
+++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt
@@ -16,22 +16,15 @@
package com.google.samples.apps.nowinandroid.core.network.retrofit
-import androidx.tracing.trace
-import com.google.samples.apps.nowinandroid.core.network.BuildConfig
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
-import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import de.jensklingenberg.ktorfit.Ktorfit
+import de.jensklingenberg.ktorfit.http.GET
+import de.jensklingenberg.ktorfit.http.Query
import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import okhttp3.Call
-import okhttp3.MediaType.Companion.toMediaType
-import retrofit2.Retrofit
-import retrofit2.http.GET
-import retrofit2.http.Query
-import javax.inject.Inject
-import javax.inject.Singleton
+import me.tatarka.inject.annotations.Inject
/**
* Retrofit API declaration for NIA Network API
@@ -58,8 +51,6 @@ private interface RetrofitNiaNetworkApi {
): List
}
-private const val NIA_BASE_URL = BuildConfig.BACKEND_URL
-
/**
* Wrapper for data provided from the [NIA_BASE_URL]
*/
@@ -69,26 +60,13 @@ private data class NetworkResponse(
)
/**
- * [Retrofit] backed [NiaNetworkDataSource]
+ * [Ktrofit] backed [NiaNetworkDataSource]
*/
-@Singleton
internal class RetrofitNiaNetwork @Inject constructor(
- networkJson: Json,
- okhttpCallFactory: dagger.Lazy,
+ ktorfit: Ktorfit,
) : NiaNetworkDataSource {
- private val networkApi = trace("RetrofitNiaNetwork") {
- Retrofit.Builder()
- .baseUrl(NIA_BASE_URL)
- // We use callFactory lambda here with dagger.Lazy
- // to prevent initializing OkHttp on the main thread.
- .callFactory { okhttpCallFactory.get().newCall(it) }
- .addConverterFactory(
- networkJson.asConverterFactory("application/json".toMediaType()),
- )
- .build()
- .create(RetrofitNiaNetworkApi::class.java)
- }
+ private val networkApi = ktorfit.create()
override suspend fun getTopics(ids: List?): List =
networkApi.getTopics(ids = ids).data
diff --git a/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt b/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt
index a0c60fdcb..b64c06e52 100644
--- a/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt
+++ b/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt
@@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.core.network.fake
-import JvmUnitTestFakeAssetManager
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -27,6 +26,7 @@ import kotlinx.datetime.toInstant
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Test
+import kotlin.test.BeforeTest
import kotlin.test.assertEquals
class FakeNiaNetworkDataSourceTest {
@@ -35,12 +35,11 @@ class FakeNiaNetworkDataSourceTest {
private val testDispatcher = StandardTestDispatcher()
- @Before
+ @BeforeTest
fun setUp() {
subject = FakeNiaNetworkDataSource(
ioDispatcher = testDispatcher,
networkJson = Json { ignoreUnknownKeys = true },
- assets = JvmUnitTestFakeAssetManager,
)
}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 5d8a65d44..084700bf2 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -33,8 +33,8 @@ dependencies {
api(projects.core.model)
implementation(libs.androidx.browser)
- implementation(libs.coil.kt)
- implementation(libs.coil.kt.compose)
+ implementation(libs.coil)
+ implementation(libs.coil.compose)
androidTestImplementation(projects.core.testing)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7ec63b6ad..f9821a38f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -119,10 +119,11 @@ androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiaut
androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
-coil-kt = { group = "io.coil-kt", name = "coil3", version.ref = "coil" }
-coil-core = { group = "io.coil-kt", name = "coil-core", version.ref = "coil" }
-coil-kt-compose = { group = "io.coil-kt", name = "coil-compose-core", version.ref = "coil" }
-coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
+coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" }
+coil-core = { group = "io.coil-kt.coil3", name = "coil-core", version.ref = "coil" }
+coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose-core", version.ref = "coil" }
+coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
+coil-network-ktor = { group = "io.coil-kt.coil3", name = "coil-network-ktor", version.ref = "coil" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }