Skip to content

Commit 2368a64

Browse files
feat(kmp): Phase 2 - multiplatform :core:network foundation
- New KMP module :core:network (android + desktop) introducing the jvmShared intermediate source set pattern: OkHttp (a JVM library) lives in jvmShared and is shared by both JVM targets, while commonMain holds the platform-agnostic NetworkClient/NetworkResponse API + an expect httpClient() factory. - OkHttpNetworkClient provides the JVM actual; an iOS (Kotlin/Native) target would add an iosMain actual (e.g. Ktor Darwin) without touching this API. - desktopApp now performs a real HTTP GET through the shared NetworkClient. Additive and non-invasive: the existing core:common NetworkHelper and the source-api network ABI are untouched (migrating them onto this foundation is follow-on work, entangled with the source-api commonMain ABI). Co-authored-by: Cuong-Tran <cuong-tran@users.noreply.github.com>
1 parent db21eb9 commit 2368a64

7 files changed

Lines changed: 154 additions & 11 deletions

File tree

core/network/build.gradle.kts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2+
3+
plugins {
4+
id("mihon.library")
5+
kotlin("multiplatform")
6+
}
7+
8+
kotlin {
9+
androidTarget()
10+
// KMK --> Desktop (JVM) target sharing the OkHttp implementation with Android via `jvmShared`
11+
jvm("desktop")
12+
// KMK <--
13+
14+
applyDefaultHierarchyTemplate()
15+
16+
sourceSets {
17+
val commonMain by getting {
18+
dependencies {
19+
api(project.dependencies.platform(kotlinx.coroutines.bom))
20+
api(kotlinx.coroutines.core)
21+
}
22+
}
23+
24+
// Intermediate source set shared by the two JVM targets (Android + desktop). OkHttp is a
25+
// JVM library, so its usage lives here rather than in commonMain. An iOS target would add a
26+
// separate `iosMain` actual (e.g. backed by Ktor) without touching this code.
27+
val jvmShared by creating {
28+
dependsOn(commonMain)
29+
dependencies {
30+
api(libs.okhttp.core)
31+
}
32+
}
33+
34+
getByName("androidMain").dependsOn(jvmShared)
35+
getByName("desktopMain").dependsOn(jvmShared)
36+
}
37+
38+
@OptIn(ExperimentalKotlinGradlePluginApi::class)
39+
compilerOptions {
40+
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
41+
}
42+
}
43+
44+
android {
45+
namespace = "tachiyomi.core.network"
46+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package tachiyomi.core.network
2+
3+
/**
4+
* Minimal multiplatform HTTP client abstraction.
5+
*
6+
* The Android and desktop targets share a single OkHttp-backed implementation (see `jvmShared`).
7+
* A future iOS (Kotlin/Native) target would provide its own actual, e.g. backed by Ktor's Darwin
8+
* engine, without changing this common API.
9+
*/
10+
interface NetworkClient {
11+
suspend fun get(url: String, headers: Map<String, String> = emptyMap()): NetworkResponse
12+
}
13+
14+
/**
15+
* Platform entry point that returns the default [NetworkClient] for the current target.
16+
*/
17+
expect fun httpClient(): NetworkClient
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tachiyomi.core.network
2+
3+
/**
4+
* Platform-agnostic representation of an HTTP response.
5+
*/
6+
data class NetworkResponse(
7+
val statusCode: Int,
8+
val isSuccessful: Boolean,
9+
val body: String,
10+
val headers: Map<String, String>,
11+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package tachiyomi.core.network
2+
3+
import kotlinx.coroutines.ExperimentalCoroutinesApi
4+
import kotlinx.coroutines.suspendCancellableCoroutine
5+
import okhttp3.Call
6+
import okhttp3.Callback
7+
import okhttp3.OkHttpClient
8+
import okhttp3.Request
9+
import okhttp3.Response
10+
import java.io.IOException
11+
import kotlin.coroutines.resume
12+
import kotlin.coroutines.resumeWithException
13+
14+
/**
15+
* OkHttp-backed [NetworkClient] shared by the Android and desktop (JVM) targets.
16+
*/
17+
actual fun httpClient(): NetworkClient = OkHttpNetworkClient()
18+
19+
class OkHttpNetworkClient(
20+
private val client: OkHttpClient = OkHttpClient(),
21+
) : NetworkClient {
22+
23+
override suspend fun get(url: String, headers: Map<String, String>): NetworkResponse {
24+
val requestBuilder = Request.Builder().url(url)
25+
headers.forEach { (name, value) -> requestBuilder.addHeader(name, value) }
26+
val response = client.newCall(requestBuilder.build()).await()
27+
return response.use {
28+
NetworkResponse(
29+
statusCode = it.code,
30+
isSuccessful = it.isSuccessful,
31+
body = it.body.string(),
32+
headers = it.headers.toMultimap().mapValues { (_, values) -> values.joinToString(", ") },
33+
)
34+
}
35+
}
36+
}
37+
38+
@OptIn(ExperimentalCoroutinesApi::class)
39+
private suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
40+
enqueue(
41+
object : Callback {
42+
override fun onResponse(call: Call, response: Response) {
43+
continuation.resume(response)
44+
}
45+
46+
override fun onFailure(call: Call, e: IOException) {
47+
if (continuation.isCancelled) return
48+
continuation.resumeWithException(e)
49+
}
50+
},
51+
)
52+
continuation.invokeOnCancellation { runCatching { cancel() } }
53+
}

desktopApp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ dependencies {
5252
// Shared Kotlin Multiplatform modules that also target Android.
5353
implementation(projects.i18n)
5454
implementation(projects.core.preference)
55+
implementation(projects.core.network)
5556
}
5657

5758
application {

desktopApp/src/main/kotlin/app/komikku/desktop/Main.kt

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,48 +11,60 @@ import androidx.compose.ui.Modifier
1111
import androidx.compose.ui.unit.dp
1212
import androidx.compose.ui.window.Window
1313
import androidx.compose.ui.window.application
14+
import kotlinx.coroutines.runBlocking
1415
import tachiyomi.core.common.preference.DesktopPreferenceStore
1516
import tachiyomi.core.common.preference.PreferenceStore
17+
import tachiyomi.core.network.httpClient
1618
import tachiyomi.i18n.MR
1719

1820
/**
19-
* Phase 0/1 Kotlin Multiplatform desktop entry point.
21+
* Phase 0/1/2 Kotlin Multiplatform desktop entry point.
2022
*
21-
* Proves that the Compose Multiplatform desktop toolchain works inside this repo, that a shared KMP
22-
* module (:i18n) is consumable from desktop, and that the shared `PreferenceStore` abstraction
23-
* (:core:preference) persists data on desktop via its [DesktopPreferenceStore] implementation.
23+
* Demonstrates the cumulative cross-platform foundation:
24+
* - Phase 0: Compose Multiplatform desktop toolchain + consuming the shared :i18n module.
25+
* - Phase 1: the shared `PreferenceStore` abstraction persisting data on desktop.
26+
* - Phase 2: the shared multiplatform `NetworkClient` (OkHttp) performing a real HTTP request.
2427
*/
2528
fun main() {
26-
// Real shared domain plumbing running on desktop: persist + read back a launch counter.
29+
// Phase 1: persist + read back a launch counter through the shared PreferenceStore.
2730
val preferenceStore: PreferenceStore = DesktopPreferenceStore()
2831
val launchCountPref = preferenceStore.getInt("desktop_launch_count", 0)
2932
val launchCount = launchCountPref.get() + 1
3033
launchCountPref.set(launchCount)
3134

35+
// Phase 2: perform a real HTTP request through the shared multiplatform NetworkClient.
36+
val networkStatus = runCatching {
37+
runBlocking {
38+
val response = httpClient().get("https://example.com")
39+
"HTTP ${response.statusCode} (${response.body.length} bytes)"
40+
}
41+
}.getOrElse { "failed: ${it.message}" }
42+
3243
application {
3344
Window(
3445
onCloseRequest = ::exitApplication,
3546
title = "Komikku Desktop",
3647
) {
37-
App(launchCount)
48+
App(launchCount = launchCount, networkStatus = networkStatus)
3849
}
3950
}
4051
}
4152

4253
@Composable
43-
fun App(launchCount: Int) {
54+
fun App(launchCount: Int, networkStatus: String) {
4455
MaterialTheme {
4556
Column(
4657
modifier = Modifier.fillMaxSize(),
4758
horizontalAlignment = Alignment.CenterHorizontally,
4859
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
4960
) {
50-
Text(text = "Komikku — Kotlin Multiplatform desktop (Phase 1)")
51-
// Reference the shared :i18n module's generated resources to prove cross-platform
52-
// code/resource sharing compiles for the desktop (JVM) target.
61+
Text(text = "Komikku — Kotlin Multiplatform desktop (Phase 2)")
62+
// Phase 0: shared :i18n module compiled for desktop.
5363
Text(text = "Shared i18n resources available: ${MR.strings::class.simpleName != null}")
54-
// Value persisted through the shared multiplatform PreferenceStore; increments every run.
64+
// Phase 1: value persisted through the shared PreferenceStore; increments every run.
5565
Text(text = "Desktop launches (persisted via shared PreferenceStore): $launchCount")
66+
// Phase 2: real network request via the shared OkHttp-backed NetworkClient.
67+
Text(text = "Shared NetworkClient GET example.com: $networkStatus")
5668
}
5769
}
5870
}

settings.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ include(":core:common")
5151
// KMK --> Multiplatform preference abstraction shared with desktop/iOS (Phase 1)
5252
include(":core:preference")
5353
// KMK <--
54+
// KMK --> Multiplatform networking foundation shared with desktop/iOS (Phase 2)
55+
include(":core:network")
56+
// KMK <--
5457
include(":data")
5558
include(":domain")
5659
include(":i18n")

0 commit comments

Comments
 (0)