12

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')
}
Braian Coronel
  • 22,105
  • 4
  • 57
  • 62
Mark
  • 18,730
  • 7
  • 107
  • 130
  • 1
    I don't know whether this is still relevant for you but [this project](https://github.com/jstuyts/kotlin-multiplatform-recipes) contains a complete example with all bells and whistles you might need. – Adam Arold Mar 29 '18 at 20:10
  • 1
    Seems interesting, especially when issue #10 will be resolved, thanks! – Mark Mar 30 '18 at 09:07

3 Answers3

15

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.

Mark
  • 18,730
  • 7
  • 107
  • 130
  • Thank you very much for these instructions, you saved me a lot of time. I think, we should create a feature request to support a "compile project('...')" dependency between two common modules. Do you know if there is one already? – Andi Mar 06 '18 at 20:51
  • I'm not aware of one – Mark Mar 07 '18 at 11:31
  • @Andi Can't you use `compile project(':alpha-common')` in `beta-common`? At least it is possible to use `compile "some.groupid:some.artifactid:1.0.0"` – Simon Forsberg Apr 14 '18 at 21:46
  • This is the key bit of information: _"Dependencies, e.g. from alpha on beta, is declared in the common alpha module **and all alpha platform modules**."_ – Steven Jeuris Oct 02 '18 at 11:35
  • I might be a little late to the party @SimonForsberg but, I have a answer to your question "Can't you use compile project(':alpha-common') in beta-common". Yes you can, but as you add your dependency to the common module, add the respective platform specific modules to their platform specific actual version that your common has. Hope this helps someone – andylamax Oct 27 '18 at 05:56
  • Do I understand correctly that this answer is outdated as of today? – Egor Okhterov Dec 12 '19 at 11:01
  • @Pixar The answer hasn't recently been tested by me so I don't know. I would hope it has gotten easier in the last two years. Although the accompanying Github is still gaining stars so I guess some of it is still correct... – Mark Dec 12 '19 at 11:53
  • @Mark I'm just struggling to set up my project for a week already and wasn't able to find any project in the internet that works, which then I could emulate. – Egor Okhterov Dec 12 '19 at 13:28
  • @Pixar Yeah it was tricky at least in 2018, I'm not aware of any new sources, be sure to post your solution if you find it. – Mark Dec 12 '19 at 14:31
0

For anyone coming from a Google search for:

"Actual function/class/companion object/etc. has no corresponding expected declaration"

Try cleaning and rebuilding the project, for me, this revealed a number of other errors which were preventing the build.

CampbellMG
  • 2,090
  • 1
  • 18
  • 27
  • 1
    I came upon this from a search for that error. What the error means is that the compiler can't find the non platform specific function/class etc that the `actual` function/class is a platform specific implementation of. – Joost de Vries May 05 '19 at 06:52
0

Submodules have to be declared within the scope of dependencies by sourceSets. If the submodule is defined in the common sourceSet, it does not need to be defined in the one of the particular platform, but not vice versa.

build.gradle.kts (:kmm_shared:feature_a)

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                api(project(":kmm_shared:domain"))
            }
        }
}

With this it is not necessary that the android sourceSet also includes the dependency.

GL

Braian Coronel
  • 22,105
  • 4
  • 57
  • 62