Skip to content

Commit d174de4

Browse files
committed
update: first modules detailed
1 parent 23ee38a commit d174de4

File tree

1 file changed

+212
-10
lines changed

1 file changed

+212
-10
lines changed

topics/development/multiplatform-project-agp-9-migration.md

Lines changed: 212 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,162 @@ In the following guide, we highlight changes related to this as well.
1717

1818
In this guide, we show how to restructure a combined multiplatform module into discrete modules clearly delineating
1919
shared logic, shared UI, and individual entry points.
20-
The example project is the default template project produced by a previous version of the KMP IDE plugin,
21-
with the unified `composeApp` module that contains all of the shared logic and shared UI code.
2220

23-
Shared UI code and shared logic code are separated to demonstrate the most flexible and scalable project structure
24-
that helps isolate UI implementation from the rest of shared code.
25-
<!-- TODO I'm still not sure when this is beneficial, need details here. -->
26-
If your project is simple enough, it might suffice to combine all shared code in a single module.
21+
The example project is a Compose Multiplatform app that is the result of the [](compose-multiplatform-new-project.md)
22+
tutorial.
23+
You can check out the initial state of the project in the [update_october_2025 branch](https://github.com/kotlin-hands-on/get-started-with-cm/tree/update_october_2025)
24+
of the sample project.
2725

28-
<!-- TODO: will people have access to the older template somewhere? Can we save it in a branch on GitHub or something so that
29-
people could compare their own project and follow the instructions? -->
26+
The example consists of a single Gradle module (`composeApp`) that contains all the shared code and all of the KMP entry points.
27+
You will extract shared code and entry points into separate modules to reach two goals:
28+
29+
* Create a more flexible and scalable project structure that allows managing shared logic, shared UI, and different entry points
30+
separately.
31+
* Isolate the Android module (that uses the `androidApplication` Gradle plugin) from KMP modules (that use the `androidLibrary`
32+
Gradle plugin).
33+
34+
For general modularization advice, see [Android modularization intro](https://developer.android.com/topic/modularization).
35+
In these terms, you are going to create several **app modules**, for each platform, and shared **feature modules**, for UI and business logic.
36+
37+
> If your project is simple enough, it might suffice to combine all shared code (shared logic and UI) in a single module.
38+
> We'll separate them to illustrate the modularisation pattern.
39+
>
40+
{style="note"}
3041

3142
### Create a shared logic module
3243

33-
You will isolate code implementing shared business logic in a `sharedLogic` module:
44+
Before actually creating a module, you need to decide on what is business logic, which code is both UI- and platform-independent.
45+
In this example, the only clear candidate is the `currentTimeAt()` function that returns exact time for a pair of location and time zone.
46+
The `Country` data class, for example, relies on `DrawableResource` from Compose Multiplatform and can't be separated from UI code.
47+
48+
Isolate the corresponding code in a `sharedLogic` module:
3449

3550
1. Create the `sharedLogic` directory at the root of the project.
51+
2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file.
52+
3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file:
53+
54+
```kotlin
55+
include(":sharedLogic")
56+
```
57+
4. Configure the Gradle build script for the new module.
58+
59+
1. In `gradle/libs.versions.toml`, add the Android Gradle Library plugin to your version catalog:
60+
61+
```text
62+
[plugins]
63+
androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
64+
```
65+
66+
2. In `sharedLogic/build.gradle.kts`, specify the plugins necessary for the shared logic module:
67+
68+
```kotlin
69+
plugins {
70+
alias(libs.plugins.kotlinMultiplatform)
71+
alias(libs.plugins.androidMultiplatformLibrary)
72+
}
73+
```
74+
3. Make sure these plugins are mentioned in the **root** `build.gradle.kts` file:
75+
76+
```kotlin
77+
plugins {
78+
alias(libs.plugins.androidMultiplatformLibrary) apply false
79+
alias(libs.plugins.kotlinMultiplatform) apply false
80+
// ...
81+
}
82+
```
83+
4. In the `kotlin {}` block, specify the targets that the common module should support in this example:
84+
85+
```kotlin
86+
kotlin {
87+
// There's no need for framework configuration since sharedLogic is not going to be exported as a framework,
88+
// only sharedUi is.
89+
iosArm64()
90+
iosSimulatorArm64()
91+
92+
jvm()
93+
94+
js {
95+
browser()
96+
}
97+
98+
@OptIn(ExperimentalWasmDsl::class)
99+
wasmJs {
100+
browser()
101+
}
102+
}
103+
```
104+
5. Add the `androidLibrary` configuration to the `kotlin {}` block. This is basically an `androidApplication` configuration
105+
with unnecessary parts removed:
106+
107+
```kotlin
108+
kotlin {
109+
// ...
110+
androidLibrary {
111+
namespace = "com.jetbrains.greeting.demo.sharedLogic"
112+
compileSdk = libs.versions.android.compileSdk.get().toInt()
113+
114+
defaultConfig {
115+
minSdk = libs.versions.android.minSdk.get().toInt()
116+
}
117+
118+
compilerOptions {
119+
sourceCompatibility = JavaVersion.VERSION_11
120+
targetCompatibility = JavaVersion.VERSION_11
121+
}
122+
}
123+
}
124+
```
125+
126+
5. Move the business logic code identified in the beginning:
127+
1. Create a `commonMain/kotlin` directory inside `sharedLogic/src`.
128+
2. Inside `commonMain/kotlin` create the `CurrentTime.kt` file.
129+
3. Copy the `currentTimeAt` function to that file. You'll see that imports are missing: there are no dependencies
130+
yet declared for the `sharedLogic` module.
131+
6. In `sharedLogic/build.gradle.kts`, add the necessary time dependencies for the common and JavaScript source sets in the same
132+
way they are declared for `composeApp`:
133+
134+
```kotlin
135+
kotlin {
136+
sourceSets {
137+
commonMain.dependencies {
138+
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
139+
}
140+
wasmJsMain.dependencies {
141+
implementation(npm("@js-joda/timezone", "2.22.0"))
142+
}
143+
}
144+
}
145+
```
146+
147+
1. `currentTimeAt`, but not `Country` which depends on CMP resources.
148+
2. Add the dependency for datetime, import the imports.
149+
3. Depend composeApp on sharedLogic to properly reference the function in its new place.
150+
6. Greeting and Platform are not used, but they are strictly speaking logic, so let's move them to sharedLogic.
151+
7. expect-actuals will break, so we need to copy the actuals to sharedLogic as well — but not entry points.
152+
8. So delete everything except the necessary actuals.
153+
9. webMain is not necessary at all, because it only serves web entry points.
154+
10. Sync Gradle check that there are no expect-actual errors.
36155

37156
### Create a shared UI module
38157

39158
You will isolate code implementing shared business logic in a `sharedUi` module:
40159

41-
1. Create the `sharedLogic` directory at the root of the project.
160+
1. Create the `sharedUi` directory at the root of the project.
161+
2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file.
162+
3. Add the new module to `settings.gradle.kts`.
163+
4. TODO removing unneeded lines from the old script seems like more work.
164+
Probably easier to give a ready-made script and comment on the changes.
165+
The most notable change is androidApplication → androidLibrary.
166+
Also we don't have any platform-specific code here, so we can remove the platform-specific dependencies.
167+
Maybe notable changes should be summarized in the beginning.
168+
5. Move resource files to sharedUi.
169+
6. Now we move code.
170+
1. Create commonMain, create App.kt with an intermediary App2() function.
171+
1. Move everything that was left after removing logic.
172+
2. Reimport the resources.
173+
3. Change the function call to App2() at entry points, check that everything is working.
174+
4. Remove the App.kt file in composeApp.
175+
5. Change the function name App2 → App.
42176
43177
### Create modules for each app entry point
44178
@@ -48,10 +182,78 @@ on the same level of the project hierarchy.
48182

49183
#### androidApp
50184

185+
1. Navigate to the composeApp/src/androindMain directory.
186+
1. Delete Platform.android.kt, since you moved it to sharedLogic already.
187+
2. Create a new `androidApp` directory at the root.
188+
2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file.
189+
3. For the build script:
190+
4. kotlinAndroid, androidApplication, composeMultiplatform, composeCompiler, no HotReload
191+
4. There is no Kotlin Android plugin explicitly defined, so add it to gradle/libs.versions.toml.
192+
5. And add the Kotlin Android plugin to the root build.gradle.kts.
193+
1. sharedUi and sharedLogic dependencies (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)`
194+
6. To androidApp/build.gradle.kts, copy everything Android-specific:
195+
7. activity dependency
196+
8. uiToolingPreview
197+
8. android {} root block
198+
9. Make sure that kotlin {} and android {} have the same JVM version to avoid mismatch.
199+
7. Copy the entire androidMain to androidApp/src
200+
9. Rename androidMain to `main`.
201+
8. Sync Gradle.
202+
9. Run MainActivity from the gutter.
203+
10. Remove androidMain from composeApp.
204+
51205
#### desktopApp
52206

207+
1. Navigate to the composeApp/src/jvmMain directory.
208+
1. Delete Platform.jvm.kt, since you moved it to sharedLogic already.
209+
2. Create a new `desktopApp` directory at the root.
210+
2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file.
211+
3. Register the desktopApp module in the root settings.gradle.kts.
212+
3. For the build script, we keep all plugins — Hot Reload, Compose, and JVM.
213+
4. There is no JVM plugin explicitly defined, so you need to add it to gradle/libs.versions.toml.
214+
5. And apply the JVM plugin in the root build.gradle.kts.
215+
6. Create main/kotlin inside src.
216+
7. Copy the package with main.kt there.
217+
9. Copy the compose.desktop {} block (here it's important that you kept the package name the same).
218+
9. Add the necessary dependencies to the build script (copy them from jvmMain.dependencies in composeApp).
219+
8. sharedUi and sharedLogic (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)`
220+
10. Run the desktop app from the gutter.
221+
11. Remove the jvmMain directory from composeApp.
222+
12. Remove the `jvm()` target from composeApp and corresponding dependencies from sourceSets{} (23:49).
223+
53224
#### webApp
54225
226+
1. You don't need to keep composeApp/jsMain and composeApp/wasmMain at all, since all they contain is actuals for Platform.
227+
Delete the corresponding directories.
228+
2. Create a new `webApp` directory at the root.
229+
2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file.
230+
3. Register the webApp module in the root settings.gradle.kts.
231+
3. For the build script, we keep all plugins — Multiplatform and Compose.
232+
4. Copy js and wasmJs targets into the new build.gradle.kts.
233+
6. Copy webMain to webApp/src
234+
7. Copy dependencies for webMain, and depend on sharedUi and sharedLogic + compose.ui.
235+
8. In the webMain/resources/index.html file, rename the script to webApp.js, to reflect the new module that will be compiled into JavaScript.
236+
8. Run the app.
237+
9. Delete the webMain directory in the composeApp folder.
238+
55239
### Update the iOS integration
56240

241+
For iOS, you don't need a separate module, so the source set can be embedded in `sharedUi`:
242+
243+
1. Move the `iosMain` directory into the `sharedUi/src` directory.
244+
2. Open the
245+
2. Then rewrite the build script in Xcode to use the Gradle task for the correct module:
246+
3. Open Xcode project for iosApp.
247+
4. Navigate to Build Phases.
248+
5. Find the Compile Kotlin Framework step.
249+
6. Change `./gradlew :composeApp:...` to `./gradlew :sharedUi:...`
250+
7. Note that the import in ContentView.swift will stay the same, because it matches the baseName parameter from Gradle configuration,
251+
not actual name of the module.
252+
253+
### Remove composeApp
254+
255+
Now that me moved all code outside composeApp, check that there are no dependencies left. Then remove composeApp entirely.
256+
257+
If you already have a `shared` module, then this will be the `sharedLogic` module,
258+
57259
## Anything else?

0 commit comments

Comments
 (0)