Search code examples
kotlingradlekotlin-multiplatformgradle-dependencieskmm

Use Gradle sub-projects with Kotlin multiplatform


I'm using Kotlin multi-platform (JVM & JS), which in IDEA creates three projects: demo, demo-js and demo-jvm.

I would like to split the common code into more subprojects/submodules. Let's say I add commonmod; how do I make it compile?

The error right now, for gradle run -p demo-jvm, is:

demo/demo-js/src/main/kotlin/demo/commonmod/example.kt: (3, 12): Actual function 'getPlatform' has no corresponding expected declaration

but I think I'm doing this fundamentally wrong, as I don't know what should depend on what (although I tried quite some iterations). If I solve this error I get other ones, and then other ones again, until I'm back to this one.


As a minimal-but-still-large example, I have:

demo/settings.gradle:

rootProject.name = 'demo'

include 'demo-jvm', 'demo-js', 'commonmod'

demo/build.gradle:

buildscript { ... }

apply plugin: 'kotlin-platform-common'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
    compile project(':commonmod')
}

demo/demo-jvm/settings.gradle:

rootProject.name = 'demo'

demo/demo-jvm/build.gradle:

buildscript { ... }

apply plugin: 'kotlin-platform-jvm'
apply plugin: 'application'

repositories {
    mavenCentral()
}

mainClassName = "demo.MainKt"

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    expectedBy project(":")
    testCompile "junit:junit:4.12"
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

demo/demo-js/settings.gradle:

rootProject.name = 'demo'

demo/demo-js/build.gradle:

buildscript { ... }

apply plugin: 'kotlin-platform-js'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
    expectedBy project(":")
    testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version"
}

demo/commonmod/settings.gradle:

rootProject.name = 'demo'

include 'demo-jvm', 'demo-js'

demo/commonmod/build.gradle:

buildscript { ... }

apply plugin: 'kotlin-platform-common'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
    compile project(':demo-js')
    compile project(':demo-jvm')
}

Solution

  • This took a crazy amount of time, so I hope this is useful for someone!

    There is a functional example on Github: kotlin_multiplatform_gradle_demo

    Several sources helped, but a lot of it was trial and error, so if something is bad practise, please let me know!


    For the minimal example, the structure is like this:

    ├── alpha
    │   ├── alpha-js
    │   │   └── build.gradle
    │   ├── alpha-jvm
    │   │   └── build.gradle
    │   ├── build.gradle
    │   └── src
    │       └── main
    │           ├── kotlin
    │           │   └── demo
    │           │       └── alpha
    │           │           └── main.kt
    ├── beta
    │   ├── beta-js
    │   │   ├── build.gradle
    │   │   └── src
    │   │       └── main
    │   │           └── kotlin
    │   │               └── demo
    │   │                   └── beta
    │   │                       └── platform.kt
    │   ├── beta-jvm
    │   │   ├── build.gradle
    │   │   └── src
    │   │       └── main
    │   │           └── kotlin
    │   │               └── demo
    │   │                   └── beta
    │   │                       └── platform.kt
    │   ├── build.gradle
    │   └── src
    │       └── main
    │           └── kotlin
    │               └── demo
    │                   └── beta
    │                       └── platform.kt
    ├── build.gradle
    └── settings.gradle
    

    The common modules (alpha and beta) need platform modules for each platform with at least a `build.gradle``.

    The settings.gradle file imports all modules, including platform ones.

    Dependencies, e.g. from alpha on beta, is declared in the common alpha module and all alpha platform modules.


    Some patterns I learned:

    • Every 'normal' (common) module has one platform module for each platform.
    • For common module alpha, the javascript platform module must be called alpha-js (similar for -jvm).
    • If there is no platform specific code, this module can be just a gradle file in a directory.
    • The platform modules can be conveniently placed inside of the common module directory (so alpha:alpha-js).
    • The common module should not refer to the platform modules; the platform modules have a dependency expectedBy project(":the_common_module").
    • If module alpha depends on beta, then

      • alpha must have dependencies { compile project(":beta") }
      • alpha-js must have dependencies { compile project(":beta:beta-js") } (in addition to expectedBy)
      • alpha-jvm must have dependencies { compile project(":beta:beta-jvm") } (in addition to expectedBy) etc
    • Only the top module has settings.gradle, which includes ALL submodules (including platform ones).

    • Make sure to get the names right, as incorrect ones don't cause an error, they just fail silently. (It seems ridiculous but I guess there is a reason.)
    • Do NOT put all the output into a single, shared build directory - this results in several weird non-deterministic errors.

    I used to have the full config files here, but it's better to just check the code on Github because it's really long.