Kotlin Multiplatform: sharing actual class implementation for multiple targets (iOS, macOS)
Asked Answered
G

4

5

I am working on a Kotlin/Native Multiplatform project that supports JVM, iOS, and macOS. My setup has the following modules:

- common
- ios
- jvm
- macos

I want to use some native code as an actual class and put an expected class in common. However, the actual class implementation is identical for multiple targets (iOS and macOS). Is there a way I can set up my sources (maybe in Gradle) so that I don't have to maintain 2 identical copies of the actual class?

Gorgonian answered 25/6, 2019 at 19:30 Comment(2)
Can you expand on this "However, the actual class implementation is identical for each target (iOS, JVM, and MacOS)". If they're identical, why are you doing expect/actual? I can see iOS and MacOS being the same, but all three?Ammo
@KevinGalligan edited with clarification. You are correct, iOS and macOS are the same but JVM is different.Gorgonian
A
5

Stately has a fairly involved config. iOS and Macos share all of the same code.

To structure the project, there's commonMain, nativeCommonMain depends on that, and actually appleMain which depends on nativeCommonMain.

commonMain {
    dependencies {
        implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
    }
}

jvmMain {
    dependsOn commonMain
    dependencies {
        implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
    }
}

nativeCommonMain {
    dependsOn commonMain
}

appleMain {
    dependsOn nativeCommonMain
}

configure([iosX64Main, iosArm64Main, macosMain, iosArm32Main]) {
    dependsOn appleMain
}

That structure is probably deeper than you need, but we needed something for linux and windows that was different. Egor's answer above is easier to follow I think.

We actually define multiplatform atomics in Stately, so you can use them as inspiration or actually just use the library itself.

https://github.com/touchlab/Stately

Common

expect class AtomicInt(initialValue: Int) {
  fun get(): Int
  fun set(newValue: Int)
  fun incrementAndGet(): Int
  fun decrementAndGet(): Int

  fun addAndGet(delta: Int): Int
  fun compareAndSet(expected: Int, new: Int): Boolean
}

JVM

actual typealias AtomicInt = AtomicInteger

Native

actual class AtomicInt actual constructor(initialValue:Int){
  private val atom = AtomicInt(initialValue)

  actual fun get(): Int = atom.value

  actual fun set(newValue: Int) {
    atom.value = newValue
  }

  actual fun incrementAndGet(): Int = atom.addAndGet(1)

  actual fun decrementAndGet(): Int = atom.addAndGet(-1)

  actual fun addAndGet(delta: Int): Int = atom.addAndGet(delta)

  actual fun compareAndSet(expected: Int, new: Int): Boolean = atom.compareAndSet(expected, new)

}
Ammo answered 26/6, 2019 at 12:15 Comment(0)
C
4

In Okio, we declare two additional source sets, nativeMain and nativeTest, and configure the built in native source sets to depend on them:

apply plugin: 'org.jetbrains.kotlin.multiplatform'

kotlin {
  iosX64()
  iosArm64()
  linuxX64()
  macosX64()
  mingwX64('winX64')
  sourceSets {
    nativeMain {
      dependsOn commonMain
    }
    nativeTest {
      dependsOn commonTest
    }

    configure([iosX64Main, iosArm64Main, linuxX64Main, macosX64Main, winX64Main]) {
      dependsOn nativeMain
    }
    configure([iosX64Test, iosArm64Test, linuxX64Test, macosX64Test, winX64Test]) {
      dependsOn nativeTest
    }
  }
}
Cleanser answered 26/6, 2019 at 1:59 Comment(0)
B
0

If all three implementations are identical, just put that code in common. expect/actual is only used for things that are different on different platforms

Bartolemo answered 25/6, 2019 at 20:48 Comment(1)
I originally misspoke. I meant to say the iOS and macOS target implementations are identical. JVM is different. I want to use an atomic integer which I can use AtomicInt for iOS and macOS but need to use AtomicInteger for JVMGorgonian
C
0

If you use Kotlin DSL your build.gradle.kts file might look like this:

kotlin {
    android()
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "yourframeworkname"
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                ...
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting
        val androidTest 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 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)
        }
    }
}
Castalia answered 13/12, 2022 at 20:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.