I'm working on a project where the web service and mobile clients share as much Kotlin code as possible. The "api"
and "client"
subprojects have a dependency on the "models"
subproject.
The API subproject is the web service. As far as I understand it, it consumes the JVM output from the Model subproject.
The Client subproject is for consumption by the iOS, macOS, and Android apps. Again, according to my kindergarten-level understanding, the mobile apps consume Kotlin/Native and Kotlin/JVM output.
include("models", "api", "client")
Abbreviated gradle.build.kts
for "client"
plugins {
alias(libs.plugins.kotlin.kmp)
...
}
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "AwesomeFramework"
}
}
listOf(
macosArm64(),
macosX64()
).forEach {
it.binaries.framework {
baseName = "AwesomeFramework"
}
}
sourceSets {
all {
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
languageSettings.optIn("kotlin.experimental.ExperimentalObjCRefinement")
}
val commonMain by getting {
dependencies {
api(project(":models"))
....
}
}
val commonTest by getting {
dependencies { ... }
}
val androidMain by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val macosX64Main by getting
val macosArm64Main by getting
val macMain by creating {
dependsOn(commonMain)
macosX64Main.dependsOn(this)
macosArm64Main.dependsOn(this)
}
}
}
android {
...
}
Abbreviated gradle.build.kts
for "models"
plugins {
alias(libs.plugins.kotlin.kmp)
...
}
kotlin {
jvm()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "AwesomeModels"
}
}
listOf(
macosArm64(),
macosX64()
).forEach {
it.binaries.framework {
baseName = "AwesomeModels"
}
}
sourceSets {
val commonMain by getting {
dependencies { ... }
}
val commonTest by getting {
dependencies { ... }
}
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
val macosX64Main by getting
val macosArm64Main by getting
val macMain by creating {
dependsOn(commonMain)
macosX64Main.dependsOn(this)
macosArm64Main.dependsOn(this)
}
}
}
I thought setting my project up this way would allow for the Models subproject to remain fairly stable and not require rebuilds. Open to suggestions if I've made things harder on myself with this configuration.
I do not understand how dependencies { api(project(":models")) }
works for KMM.
In the Models project, I have iOS specific code in the iosMain
source set. When I build the iOS app (in Xcode), I see the Client types and the Model types that Client uses. These types are preceded with Models
(e.g., ModelsUser
).
However, I don't see methods, functions, and types the Client project doesn't use. Furthermore, the iOS specific code in the Model subproject isn't available. I think this kinda makes sense - setting the dependency to api(...)
is probably going to export the smallest number of symbols possible.
So, maybe I should also consume the Model project inside of my iOS app? In other words...
import AwesomeFramework
import AwesomeModels
However, the problem is there's nothing to provide a linkage between the two frameworks. The ModelsUser
class emitted from Client is separate and distinct from the User
class emitted from the Models subproject. 🤷🏼♂️
I could smash the Models and Client projects together, but I would like to understand why that's the most "correct" solution.
I've reached the end of my (very limited) Gradle and Kotlin Multiplatform knowledge. Looking for solutions or resources that would help fill in the gaps.
Your understanding of the project setup is correct.
In order to fully export the models
to ObjC/Swift you'll need to export the dependency:
it.binaries.framework {
baseName = "AwesomeFramework"
export(project(":models"))
}
Source: https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#export-dependencies-to-binaries
At the moment frameworks generated by Kotlin are indeed considered to contain unique declarations. Which is why your User
model from AwesomeFramework
isn't the same as the one from AwesomeModels
.
Please follow KT-42250 for updates.