From bd2059ba88c38365cef7b7233aa498aa78ae9078 Mon Sep 17 00:00:00 2001 From: lihenggui Date: Fri, 1 Mar 2024 21:19:13 -0800 Subject: [PATCH] Implement network module --- app/build.gradle.kts | 2 +- core/designsystem/build.gradle.kts | 2 +- core/network/build.gradle.kts | 22 ++++-- .../kotlin/JvmUnitTestFakeAssetManager.kt | 42 ----------- .../DiskCacheComponent.kt} | 14 ++-- .../core/network/di/FlavoredNetworkModule.kt | 7 -- .../core/network/di/ImageLoaderComponent.kt | 63 +++++++++++++++++ .../core/network/di/NetworkModule.kt | 69 +++++++------------ .../network/fake/FakeNiaNetworkDataSource.kt | 15 ++-- .../network/retrofit/RetrofitNiaNetwork.kt | 36 ++-------- .../fake/FakeNiaNetworkDataSourceTest.kt | 5 +- core/ui/build.gradle.kts | 4 +- gradle/libs.versions.toml | 9 +-- 13 files changed, 133 insertions(+), 157 deletions(-) delete mode 100644 core/network/src/commonMain/kotlin/JvmUnitTestFakeAssetManager.kt rename core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/{fake/FakeAssetManager.kt => di/DiskCacheComponent.kt} (67%) create mode 100644 core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/di/ImageLoaderComponent.kt 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" }