Kotlin Multiplatform Development
SKILL.md
Gradle Tasks
- NEVER run
buildgradle task, ALWAYS use target-specific smaller gradle tasks. - Run
./gradlew tasksto list available tasks - For iOS, prefer a single target (e.g.,
iosSimulatorArm64) unless all are needed
File Naming
Use platform suffixes to distinguish files with the same name across platforms:
FileManager.kt→ common expectFileManager.android.kt→ Android actualFileManager.ios.kt→ iOS actual
Expect/Actual Patterns
Factory Function (for class instantiation in common code)
// Common
expect class Platform()
expect fun createPlatform(): Platform
// Android
actual class Platform actual constructor()
actual fun createPlatform(): Platform = Platform()
// iOS
actual class Platform actual constructor()
actual fun createPlatform(): Platform = Platform()
Function
// Common
expect fun getPlatformName(): String
// Android
actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"
// iOS
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName + " " + UIDevice.currentDevice.systemVersion
Object (singleton)
// Common
expect object Logger {
fun log(message: String)
}
// Android
actual object Logger {
actual fun log(message: String) = Log.d("App", message)
}
// iOS
actual object Logger {
actual fun log(message: String) = NSLog(message)
}
Property
// Common
expect val isDebug: Boolean
// Android
actual val isDebug: Boolean = BuildConfig.DEBUG
// iOS
actual val isDebug: Boolean = Platform.isDebugBinary
Typealias (wrap platform types)
// Common
expect class NativeDate
// Android
actual typealias NativeDate = java.util.Date
// iOS
actual typealias NativeDate = platform.Foundation.NSDate
Interface + Factory (preferred for complex APIs)
// Common
interface DeviceInfo {
val platformName: String
val osVersion: String
}
expect fun createDeviceInfo(): DeviceInfo
// Android
class AndroidDeviceInfo : DeviceInfo {
override val platformName = "Android"
override val osVersion = Build.VERSION.RELEASE
}
actual fun createDeviceInfo(): DeviceInfo = AndroidDeviceInfo()
// iOS
class IosDeviceInfo : DeviceInfo {
override val platformName = "iOS"
override val osVersion = UIDevice.currentDevice.systemVersion
}
actual fun createDeviceInfo(): DeviceInfo = IosDeviceInfo()
Choosing a Pattern
| Pattern | Use When |
|---|---|
| Interface + Factory | Default choice. Flexible, testable, multiple implementations per platform |
| Expect/actual fun | Simple factory functions or standalone platform-specific logic |
| Expect/actual object | Singletons with a fixed API |
| Expect/actual typealias | Wrapping existing platform types |
| Expect/actual class | Only when inheriting from platform-specific base classes |
Common Mistakes
- Mismatched packages between
expect/actualdeclarations - Using
expect/actual classwhen an interface would suffice
Compilation
| Pattern | Examples |
|---|---|
compileKotlin<Target> |
compileKotlinJvm, compileKotlinJs, compileKotlinIosArm64, compileKotlinIosSimulatorArm64, compileKotlinWasmJs |
compile<BuildType>KotlinAndroid |
compileDebugKotlinAndroid, compileReleaseKotlinAndroid |
compile<SourceSet>KotlinMetadata |
compileCommonMainKotlinMetadata, compileAppleMainKotlinMetadata, compileIosMainKotlinMetadata |
Linking (Kotlin/Native)
| Pattern | Examples |
|---|---|
link<BuildType>Framework<Target> |
linkDebugFrameworkIosArm64, linkReleaseFrameworkIosSimulatorArm64, linkDebugFrameworkIosX64 |
linkDebugTest<Target> |
linkDebugTestIosArm64, linkDebugTestIosSimulatorArm64 |
Testing
| Pattern | Examples |
|---|---|
allTests |
allTests (runs all targets) |
<target>Test |
iosSimulatorArm64Test, jvmTest, jsTest, jsBrowserTest, wasmJsTest |
test<BuildType>UnitTest |
testDebugUnitTest, testReleaseUnitTest |
connected<BuildType>AndroidTest |
connectedDebugAndroidTest |
Source Set Hierarchy
Exact source sets depend on your configured targets. The plugin automatically creates intermediate source sets based on declared targets.
commonMain
├── androidMain
├── jvmMain
├── webMain
│ ├── jsMain
│ └── wasmMain
│ ├── wasmJsMain
│ └── wasmWasiMain
└── nativeMain
├── appleMain
│ ├── iosMain
│ │ ├── iosArm64Main
│ │ └── iosSimulatorArm64Main
│ ├── macosMain
│ │ ├── macosX64Main
│ │ └── macosArm64Main
│ ├── tvosMain
│ └── watchosMain
├── linuxX64Main
└── mingwX64Main
Intermediate source sets for shared logic:
webMain→ JS and Wasm targets (Kotlin 2.2.20+)nativeMain→ all Kotlin/Native targetsappleMain→ iOS, macOS, watchOS, tvOSiosMain→ all iOS targets (iosArm64, iosSimulatorArm64)
Custom Source Sets
Create custom intermediate source sets with dependsOn:
kotlin {
applyDefaultHierarchyTemplate()
sourceSets {
val jvmAndroidMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(jvmAndroidMain)
jvmMain.get().dependsOn(jvmAndroidMain)
}
}
Dependencies
Add dependencies per source set. Dependencies in parent source sets propagate to children.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("group:artifact:version")
api("group:exposed-artifact:version")
}
// Platform-specific (artifact suffix varies by library)
androidMain.dependencies {
implementation("group:artifact-<android>:version")
}
iosMain.dependencies {
implementation("group:artifact-<ios>:version")
}
// Custom source sets
// - "by getting": reference existing source sets
// - "by creating": create new custom source sets
val androidMain by getting
val jvmMain by getting
val jvmAndroidMain by creating {
dependsOn(commonMain.get())
}
jvmAndroidMain.dependencies {
implementation("group:artifact-<jvm-android>:version")
}
androidMain.dependsOn(jvmAndroidMain)
jvmMain.dependsOn(jvmAndroidMain)
}
}