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

Viewed 1371

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?

3 Answers

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)

}

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
    }
  }
}

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

Related