| name | kmp-shared-modules |
| description | How to split a Kotlin Multiplatform codebase into shared modules (domain, data, ui-shared, feature-*), keep DI boundaries clean across targets, and avoid "one giant shared module" pitfalls. Use when the `shared/` module starts ballooning. |
KMP Shared Modules
Instructions
A single :shared module works for a demo. Real apps should split by layer (:domain, :data, :ui-shared) and by feature (:feature-auth, :feature-feed). Every module is itself a KMP library.
1. Target module graph
:app-android ─┐
:app-ios ─────┼─► :ui-shared ─► :feature-feed ─► :feature-auth ─► :domain ─► :core
:app-desktop ─┘ │ │
└──► :data ◄─────┘
:domain is pure Kotlin, zero deps on Ktor/SQLDelight. Defines entities + repository interfaces.
:data implements the interfaces with Ktor, SQLDelight, kotlinx.serialization.
:ui-shared holds Compose Multiplatform screens. Depends on :feature-* modules for view-models.
- Feature modules own their view-models, navigation keys, and public composables. They never depend sideways on other feature modules — shared contracts go up into
:domain.
2. A feature module build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
androidTarget()
jvm()
listOf(iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework { baseName = "FeatureAuth" }
}
sourceSets {
commonMain.dependencies {
api(projects.domain)
implementation(projects.core)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
}
}
}
android { namespace = "com.example.feature.auth" }
Use projects.domain (the type-safe project accessor) instead of project(":domain"). Enable in settings.gradle.kts:
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3. DI boundaries
Declare a Koin module per shared module. Platform-specific bindings live in androidMain/iosMain of the owning module.
val featureAuthModule = module {
factory { AuthRepositoryImpl(get(), get()) } bind AuthRepository::class
factory { LoginViewModel(get()) }
}
startKoin {
modules(
coreModule, dataModule, domainModule,
featureAuthModule, featureFeedModule,
platformModule,
)
}
4. Public API surface
Use api(...) sparingly. Prefer implementation(...) and re-export only what consumers need. For the iOS framework in :shared, expose only the composition roots and view-model facades — every public symbol ends up in the Objective-C header.
kotlin {
sourceSets.commonMain.dependencies {
api(projects.featureAuth)
api(projects.featureFeed)
api(projects.uiShared)
}
}
5. When to promote code
Start in one module. Promote to a shared module only when:
- A second consumer appears, or
- Rebuild time drops noticeably after the split, or
- The layer boundary is about to be violated (e.g. a UI file imports Ktor).
Checklist