Multi-platform project with a JVM common target

I’m working on a JVM project that can be compiled against a couple different Java APIs. I’d like to use Kotlin’s multi-platform features for this, since they look super useful for what I’m doing.

I would like to have a common module that targets the JVM, and some JVM targets with various JDKs that use common to share code.

The build script I’m playing around with below. It can’t execute because common depends on itself, since the jvm preset has a dependency on common. I’m also not sure if there are any issues with replacing/removing the common module (e.g. there’s something going on with a metadata target, which I don’t know much about).

Is there a good way to approach this kind of multi-platform project?

settings.gradle.kts:

rootProject.name = "jdk-multiplatform-test"

enableFeaturePreview("GRADLE_METADATA")

build.gradle.kts:

plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
    // Is this a problem, since common is a default target?
    jvm("common") // JDK 6+

    jvm("javaApi0") // JDK 6+
    jvm("javaApi1") // JDK 8+

    sourceSets {
        val commonMain by getting {
            // Defined by the jvm preset, causing a circular dependency problem:
            //dependsOn(commonMain)

            dependencies {
                // May not be necessary if the jvm preset defines this.
                implementation(kotlin("stdlib"))
            }
        }

        val javaApi0Main by getting {
            dependsOn(commonMain) // May not be necessary.

            dependencies {
                // May not be necessary, since commonMain already has this.
                implementation(kotlin("stdlib")) 
            }
        }

        val javaApi1Main by getting {
            dependsOn(commonMain) // May not be necessary.

            dependencies {
                implementation(kotlin("stdlib-jdk8"))
            }
        }
    }
}
1 Like

Multiplatform is designed to have shared kotlin code target multiple different platforms, not sure you are going to be able to achieve what you are looking for with multi platform but then you don’t really need to use it at all if you are only targeting the JVM.

Why not just use the java-library gradle plugin and have multiple libs that depend on each other?

1 Like

I’m mainly after the expect/actual mechanism, and it looks like that can be used without the kotlin("multiplatform") plugin (which I didn’t realize). Now I’m wondering if there’s a good way to setup a JVM Kotlin project with multiple source sets, similar to how multi-platform projects are organized. That is, one common with expect declarations, and a few source sets with the actual implementations.

Specifically, I’m working on a plugin for Minecraft servers that could be for against a couple different server platforms/APIs (e.g. Bukkit and Sponge). They share a lot of the same constructs, like players, blocks, and items, but have different APIs for them. Expect and actual would make sharing code between the two implementations a lot better compared to what I’m doing right now, which is creating adapters and factories for all the classes I have and providing them through dependency injection. This works, but it has a lot of complexity and overhead that could be avoided.

I did not realize expect/actual could be used without multiplatform, although I wonder how it would work.

The adapters would still be needed in any case so you can create a consistent API surface, its the factories/DI you could do away with as you can just instantiate the adapters classes directly (as the correct actual implementation has been selected at compile time)

You can achieve the same thing without expect/actual by creating separate libs for the server platforms/APIs but with identical public API surfaces (your adapters with the exact same class names), compile your shared code against one of the libs (or a dummy/stub one) with the compileOnly gradle statement.

Then depend the correct one at the client level where you depend on the shared lib.

What you intent to do can be done. More common in Android vs JVM targets. The support in intellij for this is however not quite there. Gradle will compile correctly as common code will actually only be compiled when compiling the target. Unfortunately it is not possible to mark a source set as being JVM so that it can JVM methods (gradle doesn’t care, Intellij does).

3 Likes

I’m facing exactly the same issue at the moment with multiplatform project. I have common JVM source set that is used by JVM and Android targets source sets. It compiles well with Gradle, but IDEA even does not see “java” package so I can’t import “java.util.*”. Is there any solution or workaround?

2 Likes

There is no proper solution yet, please follow https://youtrack.jetbrains.com/issue/KT-28194 for updates.

1 Like

Need same ability, but for JS

1 Like

Well that’s unfortunate. As I just commented on the issue, this doesn’t seem like a very hard feature to implement. At the moment I imagine there’s extra code required in order to enforce the lack of a platform. All this would need is to have that restraint removed and a minor gradle api added.

Am I mistaken and it’s actually much more complex than that?

As far as I recall it is possible to manually set the target platform of a module (until the next gradle sync). That allows the jdk classes to be found.

That works, but then all the expect declarations throw errors in IDEA about missing implementations and when going to implement them only gives the option of putting the actual definitions in the same module.

Basically it goes back to being just three normal modules without any of the conveniences of full multiplatform support.

You can do common code without expect/actual definitions, but indeed intellij doesn’t really cater well for this approach yet. In my project I use it as it is only a small part and generally multiplatform anyway (there is a proper common module and then a sourceset shared between android and java).