Recommended Kotlin Multiplatform project structure
The overviews of basic and advanced project structure concepts should give you an understanding of source sets and dependency management. What about modules which organize the source sets and rely on the dependencies?
Optimal module structure
The optimal module structure can vary depending on your goals and necessary targets. You can analyze the output of the KMP IDE plugin wizard with different configurations and sets of targets to see how we organize projects by default.
The general approach can be outlined as follows:
The entry points for your apps should be contained in separate modules, each of which depends on necessary shared code modules.
The shared code is generally divided into business logic and UI, and the strategy is to avoid unnecessary dependencies:
If all of your apps produced by the KMP project are using shared UI code as well as shared business logic, a single
sharedmodule for all of your shared code can be sufficient.If the UI for any one of your apps is written using native code (for example, you implemented the iOS UI in pure Swift), it makes sense to separate UI code from business logic to avoid Compose Multiplatform dependencies where they are not needed. So you can have a
sharedLogicand asharedUImodule and add them as dependencies to entry point modules as needed.
If your project includes server code which should share logic with client apps, the recommended way to structure it is:
An
appfolder with entry point modules and client common code modules organized as described above.A
servermodule with the server-specific code.A
coremodule for code shared between the server and clients, such as models and validation.
If your project uses an older structure, with app entry points and shared code contained in a single module, you can follow the guides below to extract the entry points into separate modules.
Creating separate modules for app entry points
The example project that we will use to illustrate for a transition to the recommended structure is an older Compose Multiplatform sample which can be found in the old-project-structure branch of the sample repository.
The example consists of a single Gradle module (composeApp) that contains all the shared code and KMP entry points, and the iosApp folder with the iOS project code and configuration.
To extract an entry point to its own module, you need to create the module, move the code, and adjust configurations accordingly both for the new module and the common code module.
Module for the Android app entry point
Create and configure the Android app module
To create an Android app module (androidApp):
Create the
androidAppdirectory at the root of the project.Inside that directory, create an empty
build.gradle.ktsfile and thesrcdirectory.Add the new module to project settings in the
settings.gradle.ktsfile by adding this line at the end of the file:include(":androidApp")Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Configure the build script for the Android app
Configure the Gradle build script for the new module:
In the
gradle/libs.versions.tomlfile, add the Kotlin Android Gradle plugin to your version catalog:[plugins] kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }In the
androidApp/build.gradle.ktsfile, specify the plugins necessary for the Android app module:plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.androidApplication) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }Make sure all of these plugins are mentioned in the root
build.gradle.ktsfile:plugins { alias(libs.plugins.kotlinAndroid) apply false alias(libs.plugins.androidApplication) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false // ... }To add the necessary dependencies, copy existing dependencies from the
androidMain.dependencies {}block of thecomposeAppbuild script and add the dependency on thecomposeAppmodule itself. In this example, the result should look like this:kotlin { dependencies { implementation(projects.composeApp) implementation(libs.androidx.activity.compose) implementation(libs.compose.uiToolingPreview) } }Copy the entire
android {}block with Android-specific configuration from thecomposeApp/build.gradle.ktsfile to theandroidApp/build.gradle.ktsfile.Copy the compiler options from the
androidTarget {}block of thecomposeApp/build.gradle.ktsfile to thetarget {}block of theandroidApp/build.gradle.ktsfile:kotlin { target { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } }Change the configuration of the
composeAppmodule from an Android application to an Android library, since that's what it effectively becomes. IncomposeApp/build.gradle.kts:Change the reference to the Gradle plugin:
alias(libs.plugins.androidApplication)alias(libs.plugins.androidLibrary)Remove the application property lines from the
android.defaultConfig {}block:defaultConfig { applicationId = "com.jetbrains.demo" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 1 versionName = "1.0" }defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() }
Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Move the code and run the Android app
Move the
composeApp/src/androidMaindirectory into theandroidApp/src/directory, but keep in mind the code that should remain cross-platform:Entry point code, like
MainActivity.ktin our sample, must be in theandroidAppmodule to properly build the Android app.All expected and actual declarations must remain in the source sets of the common module (
composeAppin our example) to be available for all platforms. As you set up the dependency ofandroidApponcomposeApp, the declarations are going to be available in entry point code as well.
Rename the
androidApp/src/androidMaindirectory tomain.If everything is configured correctly, the imports in the
androidApp/src/main/.../MainActivity.ktfile work and the code compiles.When you're using IntelliJ IDEA or Android Studio, the IDE recognizes the new module and automatically creates a new run configuration, androidApp. If that doesn't happen, modify the composeApp Android run configuration manually:
In the run configuration dropdown, select Edit Configurations.
Find the composeApp configuration in the Android category.
In the General | Module field, change
demo.composeApptodemo.androidApp.
Start the new run configuration to make sure that the app runs as expected.
If everything works correctly, in the
composeApp/build.gradle.ktsfile, remove thekotlin.sourceSets.androidMain.dependencies {}block.
You have extracted the Android entry point to a separate module. Now update the common code module to use the new Android-KMP library plugin.
Desktop JVM app
Create and configure the desktop app module
To create a desktop app module (desktopApp):
Create the
desktopAppdirectory at the root of the project.Inside that directory, create an empty
build.gradle.ktsfile and thesrcdirectory.Add the new module to project settings in the
settings.gradle.ktsfile by adding this line:include(":desktopApp")
Configure the build script for the desktop app
To make the desktop app build script work:
In the
gradle/libs.versions.tomlfile, add the Kotlin JVM Gradle plugin to your version catalog:[plugins] kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }In the
desktopApp/build.gradle.ktsfile, specify the plugins necessary for the shared UI module:plugins { alias(libs.plugins.kotlinJvm) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }Make sure all of these plugins are mentioned in the root
build.gradle.ktsfile:plugins { alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false // ... }To add the necessary dependencies on other modules, copy existing dependencies from the
commonMain.dependencies {}andjvmMain.dependencies {}blocks of thecomposeAppbuild script. In this example the end result should look like this:kotlin { dependencies { implementation(projects.sharedLogic) implementation(projects.sharedUI) implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) } }Copy the
compose.desktop {}block with desktop-specific configuration from thecomposeApp/build.gradle.ktsfile to thedesktopApp/build.gradle.ktsfile:compose.desktop { application { mainClass = "compose.project.demo.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "compose.project.demo" packageVersion = "1.0.0" } } }Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Move the code and run the desktop app
After the configuration is complete, move the code of the desktop app to the new directory:
In the
desktopApp/srcdirectory, create a newmaindirectory.Move the
composeApp/src/jvmMain/kotlindirectory into thedesktopApp/src/main/directory: It's important that the package coordinates are aligned with thecompose.desktop {}configuration.If everything is configured correctly, the imports in the
desktopApp/src/main/.../main.ktfile are working and the code is compiling.To run your desktop app, modify the composeApp [jvm] run configuration:
In the run configuration dropdown, select Edit Configurations.
Find the composeApp [jvm] configuration in the Gradle category.
In the Gradle project field, change
ComposeDemo:composeApptoComposeDemo:desktopApp.
Start the updated configuration to make sure that the app runs as expected.
If everything works correctly:
Delete the
composeApp/src/jvmMaindirectory.In the
composeApp/build.gradle.ktsfile, remove the desktop-related code:the
compose.desktop {}block,the
jvmMain.dependencies {}block inside the KotlinsourceSets {}block,the
jvm()target declaration inside thekotlin {}block.
Web app
Create and configure the web app module
To create a desktop app module (webApp):
Create the
webAppdirectory at the root of the project.Inside that directory, create an empty
build.gradle.ktsfile and thesrcdirectory.Add the new module to project settings in the
settings.gradle.ktsfile by adding this line at the end of the file:include(":webApp")
Configure the build script for the web app
To make the desktop app build script work:
In the
webApp/build.gradle.ktsfile, specify the plugins necessary for the shared UI module:```kotlin plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) } ```Make sure all of these plugins are mentioned in the root
build.gradle.ktsfile:plugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false // ... }Copy the JavaScript and Wasm target declarations from the
composeApp/build.gradle.ktsfile into thekotlin {}block in thewebApp/build.gradle.ktsfile:kotlin { js { browser() binaries.executable() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() } }Add the necessary dependencies on other modules:
kotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedLogic) // Provides the necessary entry point API implementation(compose.ui) } } }Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Move the code and run the web app
After the configuration is complete, move the code of the web app to the new directory:
Move the entire
composeApp/src/webMaindirectory into thewebApp/srcdirectory. If everything is configured correctly, the imports in thewebApp/src/webMain/.../main.ktfile are working and the code is compiling.In the
webApp/src/webMain/resources/index.htmlfile update the script name: fromcomposeApp.jstowebApp.js.To run your web app, modify the composeApp [wasmJs] run configuration:
In the run configuration dropdown, select Edit Configurations.
Find the composeApp [wasmJs] configuration in the Gradle category.
In the Gradle project field, change
ComposeDemo:composeApptoComposeDemo:webApp.
Repeat for composeApp [js] to be able to run the JavaScript version, too.
Start the run configurations to make sure that the app runs as expected.
If everything works correctly:
Delete the
composeApp/src/webMaindirectory.In the
composeApp/build.gradle.ktsfile, remove the web-related code:the
webMain.dependencies {}block inside the KotlinsourceSets {}block,the
js {}andwasmJs {}target declarations inside thekotlin {}block.
Configure the shared module
In the example app, both UI and business logic code are being shared, so it only needs a single shared module to hold all common code: you can simply repurpose composeApp as the common code module.
The only thing you need to adjust in the Gradle configuration that is not related to connections with entry point modules is the new Android Library Gradle plugin. The new plugin is built specifically for multiplatform projects and is required to use AGP 9 and newer.
Here are the necessary changes:
In
gradle/libs.versions.toml, add the Android-KMP library plugin to your version catalog:[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }In the
composeApp/build.gradle.ktsfile, add the plugin the plugins necessary for the shared UI module:plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }In the root
build.gradle.ktsfile, add the following line to avoid conflicts in applying the plugin:alias(libs.plugins.androidMultiplatformLibrary) apply falseIn the
composeApp/build.gradle.ktsfile, instead of thekotlin.androidTarget {}block add akotlin.androidLibrary {}block:androidLibrary { namespace = "compose.project.demo.composedemo" compileSdk = libs.versions.android.compileSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } androidResources { enable = true } }Remove the root
android {}block from thecomposeApp/build.gradle.ktsfile.Remove the
androidMaindependencies, since all code was moved to the app module: delete thekotlin.sourceSets.androidMain.dependencies {}block.Check that the Android app is running as expected.
(Optional) Separate shared logic and shared UI
If some of the targets in your project implement native UI, it may be a good idea to separate common code into sharedLogic and a sharedUI modules, so that app modules with native UI don't need to depend on Compose Multiplatform to use shared code.
Below is an example of how you can approach this, based on the same sample app.
Create a shared logic module
Before actually creating a module, you need to decide on what is business logic, which code is both UI- and platform-independent. In this example, the only candidate is the currentTimeAt() function, which returns the exact time for a pair of location and time zone. By contrast, the Country data class relies on DrawableResource from Compose Multiplatform and cannot be separated from UI code.
Isolate the corresponding code in a sharedLogic module:
Create the
sharedLogicdirectory at the root of the project.Inside that directory, create an empty
build.gradle.ktsfile and thesrcdirectory.Add the new module to
settings.gradle.ktsby adding this line at the end of the file:include(":sharedLogic")Configure the Gradle build script for the new module.
In the
gradle/libs.versions.tomlfile, add the Android-KMP library plugin to your version catalog:[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }In the
sharedLogic/build.gradle.ktsfile, specify the plugins necessary for the shared logic module:plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) }Make sure these plugins are mentioned in the root
build.gradle.ktsfile:plugins { alias(libs.plugins.androidMultiplatformLibrary) apply false alias(libs.plugins.kotlinMultiplatform) apply false // ... }In the
sharedLogic/build.gradle.ktsfile, specify the targets that the common module should support in this example:kotlin { // There's no need for iOS framework configuration since sharedLogic // is not going to be exported as a framework, only 'sharedUI' is. iosArm64() iosSimulatorArm64() jvm() js { browser() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() } }For Android, instead of the
androidTarget {}block, add theandroidLibrary {}configuration to thekotlin {}block:kotlin { // ... androidLibrary { namespace = "com.jetbrains.greeting.demo.sharedLogic" compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } } }Add the necessary time dependencies for the common and JavaScript source sets in the same way they are declared for
composeApp:kotlin { sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") } webMain.dependencies { implementation(npm("@js-joda/timezone", "2.22.0")) } } }Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Move the business logic code identified in the beginning:
Create a
commonMain/kotlindirectory insidesharedLogic/src.Inside
commonMain/kotlin, create theCurrentTime.ktfile.Move the
currentTimeAtfunction from the originalApp.kttoCurrentTime.kt.
Make the function available to the
App()composable at its new place. To do that, declare the dependency betweencomposeAppandsharedLogicin thecomposeApp/build.gradle.ktsfile:commonMain.dependencies { implementation(projects.sharedLogic) }Run Build | Sync Project with Gradle Files again to apply the changes.
In the
composeApp/commonMain/.../App.ktfile, import thecurrentTimeAt()function to fix the code.Run the application to make sure that your new module functions properly.
You have successfully isolated the shared logic into a separate module and used it cross-platform. Next step: creating a shared UI module.
Create a shared UI module
Extract shared code implementing common UI elements in the sharedUI module:
Create the
sharedUIdirectory at the root of the project.Inside that directory, create an empty
build.gradle.ktsfile and thesrcdirectory.Add the new module to
settings.gradle.ktsby adding this line at the end of the file:include(":sharedUI")Configure the Gradle build script for the new module:
If you haven't done this for the
sharedLogicmodule, ingradle/libs.versions.toml, add the Android-KMP library plugin to your version catalog:[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }In the
sharedUI/build.gradle.ktsfile, specify the plugins necessary for the shared UI module:plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }Make sure all of these plugins are mentioned in the root
build.gradle.ktsfile:plugins { alias(libs.plugins.androidMultiplatformLibrary) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinMultiplatform) apply false // ... }In the
kotlin {}block, specify the targets that the shared UI module should support in this example:kotlin { listOf( iosArm64(), iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { // This is the name of the iOS framework you're going // to import in your Swift code. baseName = "sharedUI" isStatic = true } } jvm() js { browser() binaries.executable() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() } }For Android, instead of the
androidTarget {}block, add theandroidLibrary {}configuration to thekotlin {}block:kotlin { // ... androidLibrary { namespace = "com.jetbrains.greeting.demo.sharedUI" compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } // Enables Compose Multiplatform resources to be used in the Android app androidResources { enable = true } } }Add the necessary dependencies for the shared UI in the same way they are declared for
composeApp:kotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedLogic) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") } } }Select Build | Sync Project with Gradle Files in the main menu, or click the Gradle refresh button in the editor.
Create a new
commonMain/kotlindirectory insidesharedUI/src.Move resource files to the
sharedUImodule: the entire directory ofcomposeApp/commonMain/composeResourcesshould be relocated tosharedUI/commonMain/composeResources.In the
sharedUI/src/commonMain/kotlin directory, create a newApp.ktfile.Copy the entire contents of the original
composeApp/src/commonMain/.../App.ktto the newApp.ktfile.Temporarily comment out all code in the old
App.ktfile. This will allow you to test whether the shared UI module is working before removing the old code completely.The new
App.ktfile should work as expected, except for resource imports, which are now located in a different package. Reimport theResobject and all drawable resources with the correct path, for example:import demo.composeapp.generated.resources.mximport demo.sharedui.generated.resources.mxTo make the new
App()composable available to the entry points in your app modules that rely on it, add the dependency to the correspondingbuild.gradle.ktsfiles:kotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedUI) // ... } } }Run your apps to check that the new module works to supply app entry points with shared UI code.
Remove the
composeApp/src/commonMain/.../App.ktfile.
You have successfully moved the cross-platform UI code into a dedicated module.
Update the iOS integration
Since the iOS app entry point is not built as a separate Gradle module, you can embed the source code into any module. In this example, you can leave it inside shared:
Move the
composeApp/src/iosMaindirectory into theshared/srcdirectory.Configure the Xcode project to consume the framework produced by the
sharedmodule:Select the File | Open Project in Xcode menu item.
Click the iosApp project in the Project navigator tool window, then select the Build Phases tab.
Find the Compile Kotlin Framework phase.
Find the line starting with
./gradlewand replacecomposeAppwithsharedUi:./gradlew :shared:embedAndSignAppleFrameworkForXcodeNote that the import in the
ContentView.swiftfile needs to stay the same because it matches thebaseNameparameter from Gradle configuration of the iOS target, not the actual name of the module. If you change the framework name in theshared/build.gradle.ktsfile, you need to change the import directive accordingly.
Run the app from Xcode or using the iosApp run configuration in IntelliJ IDEA