Implement network module

pull/1323/head
lihenggui 2 years ago
parent 2a5672f6cd
commit bd2059ba88

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

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

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

@ -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 <a href="https://developer.android.com/reference/tools/gradle-api/7.3/com/android/build/api/dsl/UnitTestOptions">UnitTestOptions</a>
*/
@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()
}

@ -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?
}

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

@ -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 <a href="https://github.com/coil-kt/coil/blob/main/coil-singleton/src/main/java/coil/Coil.kt">Coil</a>
*/
@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()
}
}

@ -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 <a href="https://github.com/coil-kt/coil/blob/main/coil-singleton/src/main/java/coil/Coil.kt">Coil</a>
*/
@Provides
@Singleton
fun imageLoader(
// We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
okHttpCallFactory: dagger.Lazy<Call.Factory>,
@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(),
)
}
}

@ -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<String>?): List<NetworkTopic> =
withContext(ioDispatcher) {
assets.open(TOPICS_ASSET).use(networkJson::decodeFromStream)
networkJson.decodeFromString(TOPICS_ASSET)
}
@OptIn(ExperimentalSerializationApi::class)
override suspend fun getNewsResources(ids: List<String>?): List<NetworkNewsResource> =
withContext(ioDispatcher) {
assets.open(NEWS_ASSET).use(networkJson::decodeFromStream)
networkJson.decodeFromString(NEWS_ASSET)
}
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =

@ -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<NetworkChangeList>
}
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<T>(
)
/**
* [Retrofit] backed [NiaNetworkDataSource]
* [Ktrofit] backed [NiaNetworkDataSource]
*/
@Singleton
internal class RetrofitNiaNetwork @Inject constructor(
networkJson: Json,
okhttpCallFactory: dagger.Lazy<Call.Factory>,
ktorfit: Ktorfit,
) : NiaNetworkDataSource {
private val networkApi = trace("RetrofitNiaNetwork") {
Retrofit.Builder()
.baseUrl(NIA_BASE_URL)
// We use callFactory lambda here with dagger.Lazy<Call.Factory>
// 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<RetrofitNiaNetworkApi>()
override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> =
networkApi.getTopics(ids = ids).data

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

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

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

Loading…
Cancel
Save