This document provides a comprehensive guide to the hierarchical structure implemented in our Kotlin Multiplatform project. The hierarchy template establishes a logical organization of source sets, creating efficient code sharing between platforms with similar characteristics. This approach minimizes duplication while allowing for platform-specific implementations where necessary.
Our project hierarchy offers several key advantages:
- Optimized Code Sharing: Share code between platforms with similar characteristics while maintaining the ability to provide platform-specific implementations
- Reduced Duplication: Avoid repeating common code across multiple platforms
- Clear Organization: Establishes a consistent structure for both main and test source sets
- Simplified Maintenance: Changes to shared code automatically propagate to all dependent platforms
- Improved Build Performance: More granular dependency structure can lead to more efficient compilation
Our project implements a carefully designed hierarchical structure that organizes code based on platform similarities:
| Source Set | Description | Platform Targets |
|---|---|---|
common |
Base shared code for all platforms | All platforms |
nonAndroid |
Code shared between non-Android platforms | JVM, JS, WebAssembly, Native |
jsCommon |
Code for JavaScript-based platforms | JS, WebAssembly |
nonJsCommon |
Code for non-JavaScript platforms | JVM, Android, Native |
jvmCommon |
Code for JVM-based platforms | JVM, Android |
nonJvmCommon |
Code for non-JVM platforms | JS, WebAssembly, Native |
jvmJsCommon |
Code shared between JVM and JavaScript platforms | JVM, JS, WebAssembly |
native |
Code for all native platforms | iOS, macOS, etc. |
nonNative |
Code for non-native platforms | JS, WebAssembly, JVM, Android |
mobile |
Code for android and apple platforms | Android, Apple |
| Source Set | Description | Platform Targets |
|---|---|---|
native/apple |
Code shared across Apple platforms | iOS, macOS |
native/apple/ios |
iOS-specific code | iOS (arm64, x64, simulatorArm64) |
native/apple/macos |
macOS-specific code | macOS |
The project is configured with the following specific platform targets:
desktop(JVM target)androidTarget(Android)iosSimulatorArm64(iOS simulator on Apple Silicon)iosX64(iOS simulator on Intel)iosArm64(iOS devices)js(JavaScript with Node.js)wasmJs(WebAssembly JavaScript with browser and Node.js support)
graph TD
common[common] --> nonAndroid[nonAndroid]
common --> jsCommon[jsCommon]
common --> nonJsCommon[nonJsCommon]
common --> jvmCommon[jvmCommon]
common --> nonJvmCommon[nonJvmCommon]
common --> native[native]
common --> nonNative[nonNative]
common --> jvmJsCommon[jvmJsCommon]
nonAndroid --> jvm[JVM/desktop]
nonAndroid --> jsCommonFromNonAndroid[jsCommon]
nonAndroid --> nativeFromNonAndroid[native]
jsCommon --> js[JavaScript]
jsCommon --> wasmJs[WebAssembly JS]
nonJsCommon --> jvmCommonFromNonJs[jvmCommon]
nonJsCommon --> nativeFromNonJs[native]
jvmCommon --> android[Android]
jvmCommon --> jvmFromJvmCommon[JVM/desktop]
nonJvmCommon --> jsCommonFromNonJvm[jsCommon]
nonJvmCommon --> nativeFromNonJvm[native]
jvmJsCommon --> jvmFromJvmJs[JVM/desktop]
jvmJsCommon --> jsCommonFromJvmJs[jsCommon]
jvmJsCommon --> wasmJsFromJvmJs[WebAssembly JS]
native --> apple[apple]
apple --> ios[ios]
apple --> macos[macOS]
ios --> iosArm64[iOS Arm64]
ios --> iosX64[iOS X64]
ios --> iosSimulatorArm64[iOS Simulator Arm64]
nonNative --> jsCommonFromNonNative[jsCommon]
nonNative --> jvmCommonFromNonNative[jvmCommon]
%% Hidden connections to handle layout without introducing new edges
jsCommonFromNonAndroid -.-> jsCommon
nativeFromNonAndroid -.-> native
jvmCommonFromNonJs -.-> jvmCommon
nativeFromNonJs -.-> native
jvmFromJvmCommon -.-> jvm
jsCommonFromNonJvm -.-> jsCommon
nativeFromNonJvm -.-> native
jsCommonFromNonNative -.-> jsCommon
jvmCommonFromNonNative -.-> jvmCommon
jvmFromJvmJs -.-> jvm
jsCommonFromJvmJs -.-> jsCommon
wasmJsFromJvmJs -.-> wasmJs
classDef actual fill: #1a73e8, stroke: #000, stroke-width: 2px, color: #ffffff
classDef group fill: #90a4ae, stroke: #000, stroke-width: 1px, color: #000000
classDef hidden display: none
class jvm actual
class android actual
class js actual
class wasmJs actual
class iosArm64 actual
class iosX64 actual
class iosSimulatorArm64 actual
class macos actual
class common group
class nonAndroid group
class jsCommon group
class nonJsCommon group
class jvmCommon group
class nonJvmCommon group
class jvmJsCommon group
class native group
class apple group
class ios group
class nonNative group
class jsCommonFromNonAndroid hidden
class nativeFromNonAndroid hidden
class jvmCommonFromNonJs hidden
class nativeFromNonJs hidden
class jvmFromJvmCommon hidden
class jsCommonFromNonJvm hidden
class nativeFromNonJvm hidden
class jsCommonFromNonNative hidden
class jvmCommonFromNonNative hidden
class jvmFromJvmJs hidden
class jsCommonFromJvmJs hidden
class wasmJsFromJvmJs hidden
To apply this hierarchy to your Kotlin Multiplatform project, call the
applyProjectHierarchyTemplate() extension function within your Kotlin configuration block:
plugins {
kotlin("multiplatform") version "1.9.20"
// Other plugins...
}
kotlin {
applyProjectHierarchyTemplate()
// Platform targets configuration...
}For a complete setup with default targets, you can use the provided configuration function:
plugins {
kotlin("multiplatform") version "1.9.20"
id("com.android.library")
// Other plugins...
}
// Apply the complete configuration
configureKotlinMultiplatform()This will:
- Apply the project hierarchy template
- Configure all default targets (JVM, Android, iOS, JS, WebAssembly)
- Set up compiler options (including "-Xexpect-actual-classes")
The source set hierarchy defines inheritance relationships between source sets. When you place code in a particular source set, it becomes available to all dependent source sets.
Code placed in jvmCommon is automatically available to both android and jvm (desktop) source
sets:
// src/jvmCommonMain/kotlin/com/example/PlatformSpecific.kt
class FileManager {
fun readLocalFile(path: String): String {
// JVM-specific file reading implementation
return java.io.File(path).readText()
}
}This code is now available in both Android and desktop JVM platforms, but not in JS or native platforms.
- Start with common: Always try to place code in the
commonsource set first - Move down as needed: If platform-specific APIs are required, move to the appropriate intermediate source set
- Use expect/actual sparingly: For truly platform-specific implementations, use
expectin common andactualin platform source sets
A typical project following this hierarchy would have a structure like:
src/
├── commonMain/kotlin/ # Common code for all platforms
├── nonAndroidMain/kotlin/ # Code for non-Android platforms
├── jsCommonMain/kotlin/ # Code for JS-based platforms
├── nonJsCommonMain/kotlin/ # Code for non-JS platforms
├── jvmCommonMain/kotlin/ # Code for JVM-based platforms
├── nonJvmCommonMain/kotlin/ # Code for non-JVM platforms
├── jvmJsCommonMain/kotlin/ # Code shared between JVM and JS platforms
├── nativeMain/kotlin/ # Code for native platforms
│ ├── apple/ # Code for Apple platforms
│ │ ├── ios/ # iOS-specific code
│ │ └── macos/ # macOS-specific code
├── nonNativeMain/kotlin/ # Code for non-native platforms
├── desktopMain/kotlin/ # JVM-specific code
├── androidMain/kotlin/ # Android-specific code
├── iosMain/kotlin/ # General iOS code
├── iosArm64Main/kotlin/ # iOS device-specific code
├── iosX64Main/kotlin/ # iOS simulator (Intel) code
├── iosSimulatorArm64Main/kotlin/ # iOS simulator (Arm64) code
├── jsMain/kotlin/ # JavaScript-specific code
└── wasmJsMain/kotlin/ # WebAssembly JS-specific code
Here's an example of how code sharing works with this hierarchy:
-
Platform-agnostic code goes in
commonMain:// src/commonMain/kotlin/com/example/DataModel.kt expect class PlatformInfo { fun getPlatformName(): String } class DataRepository(private val platformInfo: PlatformInfo) { fun getWelcomeMessage(): String { return "Hello from ${platformInfo.getPlatformName()}" } }
-
JVM-common implementation in
jvmCommonMain:// src/jvmCommonMain/kotlin/com/example/DataModel.kt actual class PlatformInfo { actual fun getPlatformName(): String { return "JVM Platform" } }
-
Android-specific customization in
androidMain:// src/androidMain/kotlin/com/example/DataModel.kt actual class PlatformInfo { actual fun getPlatformName(): String { return "Android" } }
-
iOS implementation in
iosMain:// src/iosMain/kotlin/com/example/DataModel.kt actual class PlatformInfo { actual fun getPlatformName(): String { return "iOS" } }
For network code that uses platform-specific APIs:
// src/commonMain/kotlin/com/example/network/NetworkClient.kt
expect class NetworkClient() {
suspend fun fetchData(url: String): String
}
// src/jvmCommonMain/kotlin/com/example/network/NetworkClient.kt
actual class NetworkClient {
actual suspend fun fetchData(url: String): String {
// JVM-specific implementation using java.net
return URL(url).readText()
}
}
// src/jsCommonMain/kotlin/com/example/network/NetworkClient.kt
actual class NetworkClient {
actual suspend fun fetchData(url: String): String {
// JS-specific implementation using fetch API
return window.fetch(url).text()
}
}// src/commonMain/kotlin/com/example/ui/ViewModel.kt
class ProfileViewModel {
// Common business logic for all platforms
}
// src/androidMain/kotlin/com/example/ui/ProfileScreen.kt
// Android-specific Compose UI
// src/iosMain/kotlin/com/example/ui/ProfileScreen.kt
// iOS-specific SwiftUI integrationWhen adding dependencies to your project, consider the appropriate source set:
kotlin {
// Configure targets...
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
val jvmCommonMain by getting {
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
}
}
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
}
}
}
}This hierarchy implementation uses experimental Kotlin Gradle plugin APIs and requires Kotlin 1.9.20 or newer. It may be subject to change in future Kotlin releases.
While this hierarchical structure optimizes code sharing, be mindful of potential build performance impacts with very complex hierarchies. Monitor build times and adjust if necessary.
- Excessive Source Sets: Too many intermediate source sets can complicate the project and slow down builds
- Circular Dependencies: Be careful not to create circular dependencies between source sets
- Duplicated Implementations: Multiple
actualimplementations for the same platform can cause conflicts
- Minimize Platform-Specific Code: Try to write as much platform-agnostic code as possible
- Choose the Right Source Set: Place code in the most general source set that can support it
- Keep Interfaces in Common: Define interfaces in common code and implement them in platform-specific code
- Use Clear Module Boundaries: Maintain separation of concerns between modules
- Document Platform Requirements: Clearly document when code requires specific platform capabilities
This hierarchical source set structure provides a powerful framework for organizing Kotlin Multiplatform code. By thoughtfully structuring source sets, you can maximize code sharing while maintaining the flexibility to provide platform-specific implementations when needed. The clear organization also makes the codebase more maintainable and easier to navigate.
For more information, refer to the Kotlin Multiplatform documentation and Kotlin Hierarchical Project Structure documentation.