kotlin-multiplatformgradle-kotlin-dslgradle-multi-project-build

Gradle Multiproject and Kotlin Multiplatform - KMM Framework with API dependency on sibling project


Context

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.

The Problem

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.


Solution

  • 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.