From 75589d156e6cdf305dcd70e0f2418693ae25af30 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 7 Oct 2025 16:13:15 +0200 Subject: [PATCH 01/20] update: initial for AGP 9 migration --- mpd.tree | 1 + .../multiplatform-project-agp-9-migration.md | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 topics/development/multiplatform-project-agp-9-migration.md diff --git a/mpd.tree b/mpd.tree index 5533e15d..f10d63d9 100644 --- a/mpd.tree +++ b/mpd.tree @@ -106,6 +106,7 @@ + diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md new file mode 100644 index 00000000..bfb83613 --- /dev/null +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -0,0 +1,22 @@ +[//]: # (title: Migrating a Kotlin Multiplatform project to support AGP 9.0) + +Kotlin Multiplatform projects with Android targets which were created using the Android Gradle plugin version earlier than 9.0 +need to be restructured to upgrade to AGP 9.0. +Several APIs needed for KMP configuration are hidden in AGP 9.0 and eventually are going to be removed. +To solve this in the long term, we recommend updating your project structure to isolate AGP usage to an Android module. + +On top of AGP compatibility, there is an issue of migrating to the new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin). +In the following guide, we highlight changes related to this as well. + +> To make your project work with AGP 9.0 in the short term, you can manually enable the hidden APIs. +> TODO: explain how to do that. +> +{style="note"} + +## Migration guide + +In this guide, we show how to restructure a combined module (`composeApp`, produced by an older version of KMP IDE plugin) +into discrete modules clearly delineating shared logic, shared UI, and individual entry points. + + +## Anything else? \ No newline at end of file From 197fd4d3c833b2801e0812e4df6f7658fe377768 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 7 Oct 2025 17:29:26 +0200 Subject: [PATCH 02/20] update: basic structure of the page --- .../multiplatform-project-agp-9-migration.md | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index bfb83613..374cc910 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -15,8 +15,43 @@ In the following guide, we highlight changes related to this as well. ## Migration guide -In this guide, we show how to restructure a combined module (`composeApp`, produced by an older version of KMP IDE plugin) -into discrete modules clearly delineating shared logic, shared UI, and individual entry points. +In this guide, we show how to restructure a combined multiplatform module into discrete modules clearly delineating +shared logic, shared UI, and individual entry points. +The example project is the default template project produced by a previous version of the KMP IDE plugin, +with the unified `composeApp` module that contains all of the shared logic and shared UI code. +Shared UI code and shared logic code are separated to demonstrate the most flexible and scalable project structure +that helps isolate UI implementation from the rest of shared code. + +If your project is simple enough, it might suffice to combine all shared code in a single module. + + + +### Create a shared logic module + +You will isolate code implementing shared business logic in a `sharedLogic` module: + +1. Create the `sharedLogic` directory at the root of the project. + +### Create a shared UI module + +You will isolate code implementing shared business logic in a `sharedUi` module: + +1. Create the `sharedLogic` directory at the root of the project. + +### Create modules for each app entry point + +As stated in the beginning of this page, the only module that needs isolating is the Android app entry point. +But if you have other targets enabled, to make the structure transparent it's more straightforward to keep all of them +on the same level of the project hierarchy. + +#### androidApp + +#### desktopApp + +#### webApp + +### Update the iOS integration ## Anything else? \ No newline at end of file From 4d7e0ac5f0f62be839d500d0a55ec45afbe458a9 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Fri, 10 Oct 2025 11:34:03 +0200 Subject: [PATCH 03/20] update: first modules detailed --- .../multiplatform-project-agp-9-migration.md | 222 +++++++++++++++++- 1 file changed, 212 insertions(+), 10 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 374cc910..b02c3149 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -17,28 +17,162 @@ In the following guide, we highlight changes related to this as well. In this guide, we show how to restructure a combined multiplatform module into discrete modules clearly delineating shared logic, shared UI, and individual entry points. -The example project is the default template project produced by a previous version of the KMP IDE plugin, -with the unified `composeApp` module that contains all of the shared logic and shared UI code. -Shared UI code and shared logic code are separated to demonstrate the most flexible and scalable project structure -that helps isolate UI implementation from the rest of shared code. - -If your project is simple enough, it might suffice to combine all shared code in a single module. +The example project is a Compose Multiplatform app that is the result of the [](compose-multiplatform-new-project.md) +tutorial. +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) +of the sample project. - +The example consists of a single Gradle module (`composeApp`) that contains all the shared code and all of the KMP entry points. +You will extract shared code and entry points into separate modules to reach two goals: + +* Create a more flexible and scalable project structure that allows managing shared logic, shared UI, and different entry points + separately. +* Isolate the Android module (that uses the `androidApplication` Gradle plugin) from KMP modules (that use the `androidLibrary` + Gradle plugin). + +For general modularization advice, see [Android modularization intro](https://developer.android.com/topic/modularization). +In these terms, you are going to create several **app modules**, for each platform, and shared **feature modules**, for UI and business logic. + +> If your project is simple enough, it might suffice to combine all shared code (shared logic and UI) in a single module. +> We'll separate them to illustrate the modularisation pattern. +> +{style="note"} ### Create a shared logic module -You will isolate code implementing shared business logic in a `sharedLogic` 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 clear candidate is the `currentTimeAt()` function that returns exact time for a pair of location and time zone. +The `Country` data class, for example, relies on `DrawableResource` from Compose Multiplatform and can't be separated from UI code. + +Isolate the corresponding code in a `sharedLogic` module: 1. Create the `sharedLogic` directory at the root of the project. +2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: + + ```kotlin + include(":sharedLogic") + ``` +4. Configure the Gradle build script for the new module. + + 1. In `gradle/libs.versions.toml`, add the Android Gradle Library plugin to your version catalog: + + ```text + [plugins] + androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } + ``` + + 2. In `sharedLogic/build.gradle.kts`, specify the plugins necessary for the shared logic module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + } + ``` + 3. Make sure these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + // ... + } + ``` + 4. In the `kotlin {}` block, specify the targets that the common module should support in this example: + + ```kotlin + kotlin { + // There's no need for 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() + } + } + ``` + 5. Add the `androidLibrary` configuration to the `kotlin {}` block. This is basically an `androidApplication` configuration + with unnecessary parts removed: + + ```kotlin + kotlin { + // ... + androidLibrary { + namespace = "com.jetbrains.greeting.demo.sharedLogic" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + + compilerOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } + } + ``` + +5. Move the business logic code identified in the beginning: + 1. Create a `commonMain/kotlin` directory inside `sharedLogic/src`. + 2. Inside `commonMain/kotlin` create the `CurrentTime.kt` file. + 3. Copy the `currentTimeAt` function to that file. You'll see that imports are missing: there are no dependencies + yet declared for the `sharedLogic` module. +6. In `sharedLogic/build.gradle.kts`, add the necessary time dependencies for the common and JavaScript source sets in the same + way they are declared for `composeApp`: + +```kotlin +kotlin { + sourceSets { + commonMain.dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") + } + wasmJsMain.dependencies { + implementation(npm("@js-joda/timezone", "2.22.0")) + } + } +} +``` + + 1. `currentTimeAt`, but not `Country` which depends on CMP resources. + 2. Add the dependency for datetime, import the imports. + 3. Depend composeApp on sharedLogic to properly reference the function in its new place. +6. Greeting and Platform are not used, but they are strictly speaking logic, so let's move them to sharedLogic. + 7. expect-actuals will break, so we need to copy the actuals to sharedLogic as well — but not entry points. + 8. So delete everything except the necessary actuals. + 9. webMain is not necessary at all, because it only serves web entry points. +10. Sync Gradle check that there are no expect-actual errors. ### Create a shared UI module You will isolate code implementing shared business logic in a `sharedUi` module: -1. Create the `sharedLogic` directory at the root of the project. +1. Create the `sharedUi` directory at the root of the project. +2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +3. Add the new module to `settings.gradle.kts`. +4. TODO removing unneeded lines from the old script seems like more work. + Probably easier to give a ready-made script and comment on the changes. + The most notable change is androidApplication → androidLibrary. + Also we don't have any platform-specific code here, so we can remove the platform-specific dependencies. + Maybe notable changes should be summarized in the beginning. +5. Move resource files to sharedUi. +6. Now we move code. + 1. Create commonMain, create App.kt with an intermediary App2() function. + 1. Move everything that was left after removing logic. + 2. Reimport the resources. + 3. Change the function call to App2() at entry points, check that everything is working. +4. Remove the App.kt file in composeApp. +5. Change the function name App2 → App. ### Create modules for each app entry point @@ -48,10 +182,78 @@ on the same level of the project hierarchy. #### androidApp +1. Navigate to the composeApp/src/androindMain directory. +1. Delete Platform.android.kt, since you moved it to sharedLogic already. +2. Create a new `androidApp` directory at the root. +2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +3. For the build script: + 4. kotlinAndroid, androidApplication, composeMultiplatform, composeCompiler, no HotReload +4. There is no Kotlin Android plugin explicitly defined, so add it to gradle/libs.versions.toml. +5. And add the Kotlin Android plugin to the root build.gradle.kts. +1. sharedUi and sharedLogic dependencies (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)` +6. To androidApp/build.gradle.kts, copy everything Android-specific: + 7. activity dependency + 8. uiToolingPreview + 8. android {} root block + 9. Make sure that kotlin {} and android {} have the same JVM version to avoid mismatch. +7. Copy the entire androidMain to androidApp/src +9. Rename androidMain to `main`. +8. Sync Gradle. +9. Run MainActivity from the gutter. +10. Remove androidMain from composeApp. + #### desktopApp +1. Navigate to the composeApp/src/jvmMain directory. +1. Delete Platform.jvm.kt, since you moved it to sharedLogic already. +2. Create a new `desktopApp` directory at the root. +2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +3. Register the desktopApp module in the root settings.gradle.kts. +3. For the build script, we keep all plugins — Hot Reload, Compose, and JVM. + 4. There is no JVM plugin explicitly defined, so you need to add it to gradle/libs.versions.toml. + 5. And apply the JVM plugin in the root build.gradle.kts. +6. Create main/kotlin inside src. +7. Copy the package with main.kt there. +9. Copy the compose.desktop {} block (here it's important that you kept the package name the same). +9. Add the necessary dependencies to the build script (copy them from jvmMain.dependencies in composeApp). +8. sharedUi and sharedLogic (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)` +10. Run the desktop app from the gutter. +11. Remove the jvmMain directory from composeApp. +12. Remove the `jvm()` target from composeApp and corresponding dependencies from sourceSets{} (23:49). + #### webApp +1. You don't need to keep composeApp/jsMain and composeApp/wasmMain at all, since all they contain is actuals for Platform. + Delete the corresponding directories. +2. Create a new `webApp` directory at the root. +2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +3. Register the webApp module in the root settings.gradle.kts. +3. For the build script, we keep all plugins — Multiplatform and Compose. + 4. Copy js and wasmJs targets into the new build.gradle.kts. +6. Copy webMain to webApp/src +7. Copy dependencies for webMain, and depend on sharedUi and sharedLogic + compose.ui. +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. +8. Run the app. +9. Delete the webMain directory in the composeApp folder. + ### Update the iOS integration +For iOS, you don't need a separate module, so the source set can be embedded in `sharedUi`: + +1. Move the `iosMain` directory into the `sharedUi/src` directory. +2. Open the +2. Then rewrite the build script in Xcode to use the Gradle task for the correct module: + 3. Open Xcode project for iosApp. + 4. Navigate to Build Phases. + 5. Find the Compile Kotlin Framework step. + 6. Change `./gradlew :composeApp:...` to `./gradlew :sharedUi:...` + 7. Note that the import in ContentView.swift will stay the same, because it matches the baseName parameter from Gradle configuration, + not actual name of the module. + +### Remove composeApp + +Now that me moved all code outside composeApp, check that there are no dependencies left. Then remove composeApp entirely. + +If you already have a `shared` module, then this will be the `sharedLogic` module, + ## Anything else? \ No newline at end of file From 92bfc3ae1f5c038d3d34a1d5277d05a003066f3a Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Mon, 13 Oct 2025 20:14:08 +0200 Subject: [PATCH 04/20] update: done with the bulk of instructions --- .../compose-multiplatform-modify-project.md | 1 + .../compose-multiplatform-new-project.md | 48 +- .../multiplatform-project-agp-9-migration.md | 752 +++++++++++++----- 3 files changed, 596 insertions(+), 205 deletions(-) diff --git a/topics/compose-onboard/compose-multiplatform-modify-project.md b/topics/compose-onboard/compose-multiplatform-modify-project.md index 5bb896c2..076d4c3a 100644 --- a/topics/compose-onboard/compose-multiplatform-modify-project.md +++ b/topics/compose-onboard/compose-multiplatform-modify-project.md @@ -69,6 +69,7 @@ To use the `kotlinx-datetime` library: 1. Open the `composeApp/src/commonMain/kotlin/App.kt` file and add the following function which returns a string containing the current date: ```kotlin + @OptIn(ExperimentalTime::class) fun todaysDate(): String { fun LocalDateTime.format() = toString().substringBefore('T') diff --git a/topics/compose-onboard/compose-multiplatform-new-project.md b/topics/compose-onboard/compose-multiplatform-new-project.md index a608123d..d67472d7 100644 --- a/topics/compose-onboard/compose-multiplatform-new-project.md +++ b/topics/compose-onboard/compose-multiplatform-new-project.md @@ -68,7 +68,8 @@ To get started, implement a new `App` composable: When you run your application and click the button, the hardcoded time is displayed. -3. Run the application on the desktop. It works, but the window is clearly too large for the UI: +3. Run the application on the desktop (with the **composeApp [jvm]** run configuration). + It works, but the window is clearly too large for the UI: ![New Compose Multiplatform app on desktop](first-compose-project-on-desktop-3.png){width=400} @@ -77,7 +78,7 @@ To get started, implement a new `App` composable: ```kotlin fun main() = application { val state = rememberWindowState( - size = DpSize(400.dp, 250.dp), + size = DpSize(400.dp, 350.dp), position = WindowPosition(300.dp, 300.dp) ) Window( @@ -94,22 +95,22 @@ To get started, implement a new `App` composable: Here, you set the title of the window and use the `WindowState` type to give the window an initial size and position on the screen. - > To see your changes in real time in the desktop app, use [Compose Hot Reload](compose-hot-reload.md): - > 1. In the `main.kt` file, click the **Run** icon in the gutter. - > 2. Select **Run 'composeApp [hotRunJvm]' with Compose Hot Reload (Beta)**. - > ![Run Compose Hot Reload from gutter](compose-hot-reload-gutter-run.png){width=350} - > - > To see the app automatically update, save any modified files (⌘ S / Ctrl+S). - > - > Compose Hot Reload is currently in [Beta](https://kotlinlang.org/components-stability.html#stability-levels-explained) so its functionality is subject to change. - > - {style="tip"} - 5. Follow the IDE's instructions to import the missing dependencies. 6. Run the desktop application again. Its appearance should improve: ![Improved appearance of the Compose Multiplatform app on desktop](first-compose-project-on-desktop-4.png){width=350} +> To see your changes in real time in the desktop app, use [Compose Hot Reload](compose-hot-reload.md): +> 1. In the `main.kt` file, click the **Run** icon in the gutter. +> 2. Select **Run 'composeApp [hotRunJvm]' with Compose Hot Reload (Beta)**. + > ![Run Compose Hot Reload from gutter](compose-hot-reload-gutter-run.png){width=350} +> +> To see the app automatically update, save any modified files (⌘ S / Ctrl+S). +> +> Compose Hot Reload is currently in [Beta](https://kotlinlang.org/components-stability.html#stability-levels-explained) so its functionality is subject to change. +> +{style="tip"} + + +The example consists of a single Gradle module (`composeApp`) that contains all the shared code and all of the KMP entry +points. You will extract shared code and entry points into separate modules to reach two goals: -* Create a more flexible and scalable project structure that allows managing shared logic, shared UI, and different entry points +* Create a more flexible and scalable project structure that allows managing shared logic, shared UI, and different + entry points separately. -* Isolate the Android module (that uses the `androidApplication` Gradle plugin) from KMP modules (that use the `androidLibrary` +* Isolate the Android module (that uses the `androidApplication` Gradle plugin) from KMP modules (that use the + `androidLibrary` Gradle plugin). -For general modularization advice, see [Android modularization intro](https://developer.android.com/topic/modularization). -In these terms, you are going to create several **app modules**, for each platform, and shared **feature modules**, for UI and business logic. +For general modularization advice, +see [Android modularization intro](https://developer.android.com/topic/modularization). +In these terms, you are going to create several **app modules**, for each platform, and shared **feature modules**, for +UI and business logic. -> If your project is simple enough, it might suffice to combine all shared code (shared logic and UI) in a single module. +> If your project is simple enough, it might suffice to combine all shared code (shared logic and UI) in a single +module. > We'll separate them to illustrate the modularisation pattern. > {style="note"} ### 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 clear candidate is the `currentTimeAt()` function that returns exact time for a pair of location and time zone. -The `Country` data class, for example, relies on `DrawableResource` from Compose Multiplatform and can't be separated from UI code. +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 clear candidate is the `currentTimeAt()` function that returns exact time for a pair of +location and time zone. +The `Country` data class, for example, relies on `DrawableResource` from Compose Multiplatform and can't be separated +from UI code. Isolate the corresponding code in a `sharedLogic` module: 1. Create the `sharedLogic` directory at the root of the project. -2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. 3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: ```kotlin @@ -56,204 +71,575 @@ Isolate the corresponding code in a `sharedLogic` module: ``` 4. Configure the Gradle build script for the new module. - 1. In `gradle/libs.versions.toml`, add the Android Gradle Library plugin to your version catalog: + 1. In the `gradle/libs.versions.toml` file, add the Android Gradle Library plugin to your version catalog: - ```text - [plugins] - androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } + ```text + [plugins] + androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } + ``` + + 2. In the `sharedLogic/build.gradle.kts` file, specify the plugins necessary for the shared logic module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + } ``` - - 2. In `sharedLogic/build.gradle.kts`, specify the plugins necessary for the shared logic module: + 3. Make sure these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + // ... + } + ``` + 4. In the `kotlin {}` block, specify the targets that the common module should support in this example: + + ```kotlin + 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() + } + } + ``` + 5. For Android, instead of the `androidTarget {}` block, add the `androidLibrary {}` configuration to the + `kotlin {}` block: + + ```kotlin + 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 + } + } + } + ``` + 6. Add the necessary time dependencies for the common and JavaScript source sets in the same + way they are declared for `composeApp`: + + ```kotlin + kotlin { + sourceSets { + commonMain.dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-datetime:%dateTimeVersion%") + } + webMain.dependencies { + implementation(npm("@js-joda/timezone", "2.22.0")) + } + } + } + ``` + 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Move the business logic code identified in the beginning: + 1. Create a `commonMain/kotlin` directory inside `sharedLogic/src`. + 2. Inside `commonMain/kotlin`, create the `CurrentTime.kt` file. + 3. Move the `currentTimeAt` function from the original `App.kt` to `CurrentTime.kt`. +6. Make the function available to the `App()` composable at its new place. + To do that, declare the dependency between `composeApp` and `sharedLogic` in the `composeApp/build.gradle.kts` file: + + ```kotlin + commonMain.dependencies { + implementation(projects.sharedLogic) + } + ``` +7. Run **Build | Sync Project with Gradle Files** again to make the dependency work. +8. In the `composeApp/commonMain/.../App.kt` file, import the `currentTimeAt()` function to fix the code. +9. Run the application to make sure that your new module functions properly. + +You have isolated the shared logic in a separate module and successfully used it cross-platform. +Next step, creating a shared UI module. + + + +### Create a shared UI module + +Isolate shared code implementing common UI elements in the `sharedUi` module: + +1. Create the `sharedUi` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: + + ```kotlin + include(":sharedUi") + ``` +4. Configure the Gradle build script for the new module. + + 1. If you haven't done this for the `sharedLogic` module, in `gradle/libs.versions.toml`, + add the Android Gradle Library plugin to your version catalog: + + ```text + [plugins] + androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } + ``` + + 2. In the `sharedUi/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) + } + ``` + + 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.composeHotReload) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + // ... + } + ``` + + 4. In the `kotlin {}` block, specify the targets that the shared UI module should support in this example: + + ```kotlin + 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() + } + } + ``` + + 5. For Android, instead of the `androidTarget {}` block, add the `androidLibrary {}` configuration to the + `kotlin {}` block: + + ```kotlin + kotlin { + // ... + androidLibrary { + namespace = "com.jetbrains.greeting.demo.sharedLogic" + compileSdk = libs.versions.android.compileSdk.get().toInt() + minSdk = libs.versions.android.minSdk.get().toInt() - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidMultiplatformLibrary) - } - ``` - 3. Make sure these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.androidMultiplatformLibrary) apply false - alias(libs.plugins.kotlinMultiplatform) apply false - // ... - } - ``` - 4. In the `kotlin {}` block, specify the targets that the common module should support in this example: + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } + + // Enables Compose Multiplatform resources to be used in the Android app + androidResources { + enable = true + } + } + } + ``` + + 6. Add the necessary dependencies for the shared UI in the same way they are declared for `composeApp`: ```kotlin kotlin { - // There's no need for 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() + 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:%dateTimeVersion%") + } } } ``` - 5. Add the `androidLibrary` configuration to the `kotlin {}` block. This is basically an `androidApplication` configuration - with unnecessary parts removed: + 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + 5. TODO Maybe notable changes to the build script should be summarized in the beginning. +5. Create a new `commonMain/kotlin` directory inside `sharedUi/src`. +6. Move resource files to the `sharedUi` module: the entire directory of `composeApp/commonMain/composeResources` should + be relocated to `sharedUi/commonMain/composeResources`. +7. In the `sharedUi/src/commonMain/kotlin directory`, create a new `App.kt` file. +8. Copy the entire contents of the original `composeApp/src/commonMain/.../App.kt` to the new `App.kt` file. +9. Comment out all code in the old `App.kt` file in the meantime. + You'll test whether the shared UI module is working before removing old code completely. +10. Everything in the new `App.kt` file should be working except for resource imports, which are now located in a + different package. + Reimport the `Res` object and all drawable resources with the correct path, for example: + + + + import demo.composeapp.generated.resources.mx + + + import demo.sharedui.generated.resources.mx + + +11. Make the new `App()` composable available to the entry poins in the `composeApp` module. + To do that, declare the dependency between `composeApp` and `sharedUi` in the `composeApp/build.gradle.kts` file: + + ```kotlin + commonMain.dependencies { + implementation(projects.sharedLogic) + implementation(projects.sharedUi) + } + ``` +12. Run your apps to check that the new module works to supply app entry points with shared UI code. +13. Remove the `composeApp/src/commonMain/.../App.kt` file. + +You have successfully moved the cross-platform UI code to a dedicated module. +The only thing left is to create dedicated modules for every app you are producing with this project. + +### Create modules for each app entry point + +As stated in the beginning of this page, the only module that needs isolating is the Android app entry point. +But if you have other targets enabled, it's more straightforward and transparent to keep all the entry points +on the same level of the project hierarchy. + +#### Android app + +Create and configure a new entry point module for the Android app: + +1. Create the `androidApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":androidApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: + + ```text + [plugins] + kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + ``` + + 2. In the `androidApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + } + ``` + + 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.kotlinAndroid) apply false + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` + + 4. To add the necessary dependencies on other modules, copy existing dependencies from the + `commonMain.dependencies {}` and `androidMain.dependencies {}` blocks + of the `composeApp` build script. In this example the end result should look like this: ```kotlin kotlin { - // ... - androidLibrary { - namespace = "com.jetbrains.greeting.demo.sharedLogic" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } - - compilerOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } + dependencies { + implementation(projects.sharedLogic) + implementation(projects.sharedUi) + implementation(libs.androidx.activity.compose) + implementation(compose.preview) } } ``` -5. Move the business logic code identified in the beginning: - 1. Create a `commonMain/kotlin` directory inside `sharedLogic/src`. - 2. Inside `commonMain/kotlin` create the `CurrentTime.kt` file. - 3. Copy the `currentTimeAt` function to that file. You'll see that imports are missing: there are no dependencies - yet declared for the `sharedLogic` module. -6. In `sharedLogic/build.gradle.kts`, add the necessary time dependencies for the common and JavaScript source sets in the same - way they are declared for `composeApp`: - -```kotlin -kotlin { - sourceSets { - commonMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") + 5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` + file to the `androidApp/build.gradle.kts` file. + + 6. In the `kotlin {}` block, add a `compilerOptions {}` block that would match the Java version specified for + the Android application. In our example: + + ```kotlin + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } } - wasmJsMain.dependencies { - implementation(npm("@js-joda/timezone", "2.22.0")) + ``` + + 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Copy the `composeApp/src/androidMain` directory into the `androidApp/src/` directory. +6. Rename the `androidApp/src/androidMain` directory into `main`. +7. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working + and the code is compiling. +8. Run the Android app from the gutter in the `androidApp/src/main/.../MainActivity.kt` file: click the green arrow + in the `class MainActivity` line and select **Run 'MainActivity'**. +9. If everything works correctly: + * Remove the `composeApp/src/androidMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `android {}` block, + * the `androidMain.dependencies {}`, + * the `androidTarget {}` block inside the `kotlin {}` block. + +#### Desktop JVM app + +Create and configure the JVM desktop app module: + +1. Create the `desktopApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":desktopApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `gradle/libs.versions.toml` file, add the Kotlin JVM Gradle plugin to your version catalog: + + ```text + [plugins] + kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + ``` + + 2. In the `desktopApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) } - } -} -``` - - 1. `currentTimeAt`, but not `Country` which depends on CMP resources. - 2. Add the dependency for datetime, import the imports. - 3. Depend composeApp on sharedLogic to properly reference the function in its new place. -6. Greeting and Platform are not used, but they are strictly speaking logic, so let's move them to sharedLogic. - 7. expect-actuals will break, so we need to copy the actuals to sharedLogic as well — but not entry points. - 8. So delete everything except the necessary actuals. - 9. webMain is not necessary at all, because it only serves web entry points. -10. Sync Gradle check that there are no expect-actual errors. + ``` -### Create a shared UI module + 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: -You will isolate code implementing shared business logic in a `sharedUi` module: + ```kotlin + plugins { + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.composeHotReload) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` -1. Create the `sharedUi` directory at the root of the project. -2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. -3. Add the new module to `settings.gradle.kts`. -4. TODO removing unneeded lines from the old script seems like more work. - Probably easier to give a ready-made script and comment on the changes. - The most notable change is androidApplication → androidLibrary. - Also we don't have any platform-specific code here, so we can remove the platform-specific dependencies. - Maybe notable changes should be summarized in the beginning. -5. Move resource files to sharedUi. -6. Now we move code. - 1. Create commonMain, create App.kt with an intermediary App2() function. - 1. Move everything that was left after removing logic. - 2. Reimport the resources. - 3. Change the function call to App2() at entry points, check that everything is working. -4. Remove the App.kt file in composeApp. -5. Change the function name App2 → App. + 4. To add the necessary dependencies on other modules, copy existing dependencies from the + `commonMain.dependencies {}` and `jvmMain.dependencies {}` blocks + of the `composeApp` build script. In this example the end result should look like this: -### Create modules for each app entry point + ```kotlin + kotlin { + dependencies { + implementation(projects.sharedLogic) + implementation(projects.sharedUi) + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + } + } + ``` -As stated in the beginning of this page, the only module that needs isolating is the Android app entry point. -But if you have other targets enabled, to make the structure transparent it's more straightforward to keep all of them -on the same level of the project hierarchy. + 5. Copy the `compose.desktop {}` block with desktop-specific configuration from the `composeApp/build.gradle.kts` + file to the `desktopApp/build.gradle.kts` file: + + ```kotlin + compose.desktop { + application { + mainClass = "compose.project.demo.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "compose.project.demo" + packageVersion = "1.0.0" + } + } + } + ``` + 6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Move the code: In the `desktopApp/src` directory, create a new `main` directory. +6. Copy the `composeApp/src/jvmMain/kotlin` directory into the `desktopApp/src/main/` directory: + It's important that the package coordinates are aligned with the `compose.desktop {}` configuration. +7. If everything is configured correctly, the imports in the `desktopApp/src/main/.../main.kt` file are working + and the code is compiling. +8. Run your desktop app from the gutter in the `desktopApp/src/main/.../main.kt` file: click the green arrow next + to the `main()` function and select **Run 'Main.kt'**. +9. If everything works correctly: + * Remove the `composeApp/src/jvmMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `compose.desktop {}` block, + * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `jvm()` target declaration inside the `kotlin {}` block. + +#### Web app + +Create and configure the web app module: + +1. Create the `webApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":webApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + } + ``` + + 2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` + + 3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block + in the `webApp/build.gradle.kts` file: + + ```kotlin + kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + } + ``` -#### androidApp - -1. Navigate to the composeApp/src/androindMain directory. -1. Delete Platform.android.kt, since you moved it to sharedLogic already. -2. Create a new `androidApp` directory at the root. -2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. -3. For the build script: - 4. kotlinAndroid, androidApplication, composeMultiplatform, composeCompiler, no HotReload -4. There is no Kotlin Android plugin explicitly defined, so add it to gradle/libs.versions.toml. -5. And add the Kotlin Android plugin to the root build.gradle.kts. -1. sharedUi and sharedLogic dependencies (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)` -6. To androidApp/build.gradle.kts, copy everything Android-specific: - 7. activity dependency - 8. uiToolingPreview - 8. android {} root block - 9. Make sure that kotlin {} and android {} have the same JVM version to avoid mismatch. -7. Copy the entire androidMain to androidApp/src -9. Rename androidMain to `main`. -8. Sync Gradle. -9. Run MainActivity from the gutter. -10. Remove androidMain from composeApp. - -#### desktopApp - -1. Navigate to the composeApp/src/jvmMain directory. -1. Delete Platform.jvm.kt, since you moved it to sharedLogic already. -2. Create a new `desktopApp` directory at the root. -2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. -3. Register the desktopApp module in the root settings.gradle.kts. -3. For the build script, we keep all plugins — Hot Reload, Compose, and JVM. - 4. There is no JVM plugin explicitly defined, so you need to add it to gradle/libs.versions.toml. - 5. And apply the JVM plugin in the root build.gradle.kts. -6. Create main/kotlin inside src. -7. Copy the package with main.kt there. -9. Copy the compose.desktop {} block (here it's important that you kept the package name the same). -9. Add the necessary dependencies to the build script (copy them from jvmMain.dependencies in composeApp). -8. sharedUi and sharedLogic (not `implementation(project((":sharedUi")))`, but `implementation(project.sharedUi)` -10. Run the desktop app from the gutter. -11. Remove the jvmMain directory from composeApp. -12. Remove the `jvm()` target from composeApp and corresponding dependencies from sourceSets{} (23:49). - -#### webApp - -1. You don't need to keep composeApp/jsMain and composeApp/wasmMain at all, since all they contain is actuals for Platform. - Delete the corresponding directories. -2. Create a new `webApp` directory at the root. -2. Inside that directory, create the `src` directory and an empty `build.gradle.kts` file. -3. Register the webApp module in the root settings.gradle.kts. -3. For the build script, we keep all plugins — Multiplatform and Compose. - 4. Copy js and wasmJs targets into the new build.gradle.kts. -6. Copy webMain to webApp/src -7. Copy dependencies for webMain, and depend on sharedUi and sharedLogic + compose.ui. -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. -8. Run the app. -9. Delete the webMain directory in the composeApp folder. + 4. Add the necessary dependencies on other modules: + + ```kotlin + kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.sharedLogic) + // Provides the necessary entry point API + implementation(compose.ui) + } + } + } + ``` + + 5. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Copy the entire `composeApp/src/webMain` directory into the `webApp/src` directory. + If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working + and the code is compiling. +6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. +7. Run your web app from the gutter in the `webApp/src/webMain/.../main.kt` file: click the green arrow next + to the `main()` function and select **Run 'webApp [wasmJs]'**. +8. If everything works correctly: + * Remove the `composeApp/src/jvmMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `compose.desktop {}` block, + * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `jvm()` target declaration inside the `kotlin {}` block. ### Update the iOS integration -For iOS, you don't need a separate module, so the source set can be embedded in `sharedUi`: +Since the iOS app is not a separate Gradle module, you can simply move the source set into the `sharedUi` directory: + +1. Move the `composeApp/src/iosMain` directory into the `sharedUi/src` directory. +2. Configure the Xcode project to consume the framework produced by the `sharedUi` module: + 1. Select the **File | Open Project in Xcode** menu item. + 2. Click the **iosApp** project in the **Project navigator** tool window, then select the **Build Phases** tab. + 3. Find the **Compile Kotlin Framework** phase. + 4. Find the line starting with `./gradlew` and swap `composeApp` for `sharedUi`: + + ```text + ./gradlew :sharedUi:embedAndSignAppleFrameworkForXcode + ``` + + 5. Note that the import in the `ContentView.swift` file will stay the same, because it matches the `baseName` parameter from + Gradle configuration of the iOS target, + not actual name of the module. + If you change the framework name in the `sharedUi/build.gradle.kts` file, you need to change the import directive accordingly. + +4. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA + +### Remove composeApp and TODO create new run configurations? -1. Move the `iosMain` directory into the `sharedUi/src` directory. -2. Open the -2. Then rewrite the build script in Xcode to use the Gradle task for the correct module: - 3. Open Xcode project for iosApp. - 4. Navigate to Build Phases. - 5. Find the Compile Kotlin Framework step. - 6. Change `./gradlew :composeApp:...` to `./gradlew :sharedUi:...` - 7. Note that the import in ContentView.swift will stay the same, because it matches the baseName parameter from Gradle configuration, - not actual name of the module. +Now that me moved all code outside composeApp, check that there are no dependencies left. Then remove composeApp +entirely. -### Remove composeApp -Now that me moved all code outside composeApp, check that there are no dependencies left. Then remove composeApp entirely. -If you already have a `shared` module, then this will be the `sharedLogic` module, +## Anything else? -## Anything else? \ No newline at end of file +If you already have a `shared` module, then this will be the `sharedLogic` module TODO put this in the sharedLogic part. \ No newline at end of file From 66bde3c3ca88e36e1551713e3e900ee0ec1616e6 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 14 Oct 2025 11:48:57 +0200 Subject: [PATCH 05/20] update: done, with a couple of todos left up to discussion --- .../multiplatform-project-agp-9-migration.md | 102 +++++++++++------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index a50bac09..f00ec03c 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -11,7 +11,10 @@ new [Android Gradle Library plugin](https://developer.android.com/kotlin/multipl In the following guide, we highlight changes related to this as well. > To make your project work with AGP 9.0 in the short term, you can manually enable the hidden APIs. -> TODO: explain how to do that. +> To do that, in the `gradle.properties` file of your project add this property: +> `android.enableLegacyVariantApi=true`. +> +> The legacy API is going to be removed completely in AGP 10, make sure you finish the migration before that! > {style="note"} @@ -27,7 +30,8 @@ the [update_october_2025 branch](https://github.com/kotlin-hands-on/get-started- of the sample project. + and migrated the result + in the meantime, the best approach is to follow the tutorial :)--> The example consists of a single Gradle module (`composeApp`) that contains all the shared code and all of the KMP entry points. @@ -60,6 +64,11 @@ location and time zone. The `Country` data class, for example, relies on `DrawableResource` from Compose Multiplatform and can't be separated from UI code. +> If your project already has a `shared` module, for example, because you don't share the entirety of UI code, +> then you can use this module in place of `sharedLogic` — or rename it to better differentiate between shared logic and UI. +> +{style="note"} + Isolate the corresponding code in a `sharedLogic` module: 1. Create the `sharedLogic` directory at the root of the project. @@ -198,7 +207,7 @@ Isolate shared code implementing common UI elements in the `sharedUi` module: ```kotlin include(":sharedUi") ``` -4. Configure the Gradle build script for the new module. +4. Configure the Gradle build script for the new module: 1. If you haven't done this for the `sharedLogic` module, in `gradle/libs.versions.toml`, add the Android Gradle Library plugin to your version catalog: @@ -309,7 +318,6 @@ Isolate shared code implementing common UI elements in the `sharedUi` module: ``` 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the editor. - 5. TODO Maybe notable changes to the build script should be summarized in the beginning. 5. Create a new `commonMain/kotlin` directory inside `sharedUi/src`. 6. Move resource files to the `sharedUi` module: the entire directory of `composeApp/commonMain/composeResources` should be relocated to `sharedUi/commonMain/composeResources`. @@ -429,14 +437,15 @@ Create and configure a new entry point module for the Android app: 6. Rename the `androidApp/src/androidMain` directory into `main`. 7. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working and the code is compiling. -8. Run the Android app from the gutter in the `androidApp/src/main/.../MainActivity.kt` file: click the green arrow - in the `class MainActivity` line and select **Run 'MainActivity'**. -9. If everything works correctly: - * Remove the `composeApp/src/androidMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: - * the `android {}` block, - * the `androidMain.dependencies {}`, - * the `androidTarget {}` block inside the `kotlin {}` block. +8. To run your Android app, change and rename the **composeApp** Android run configuration or add a similar one. + In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. +9. Start the run configuration to make sure that the app runs as expected. +10. If everything works correctly: + * Remove the `composeApp/src/androidMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `android {}` block, + * the `androidMain.dependencies {}`, + * the `androidTarget {}` block inside the `kotlin {}` block. #### Desktop JVM app @@ -520,14 +529,15 @@ Create and configure the JVM desktop app module: It's important that the package coordinates are aligned with the `compose.desktop {}` configuration. 7. If everything is configured correctly, the imports in the `desktopApp/src/main/.../main.kt` file are working and the code is compiling. -8. Run your desktop app from the gutter in the `desktopApp/src/main/.../main.kt` file: click the green arrow next - to the `main()` function and select **Run 'Main.kt'**. -9. If everything works correctly: - * Remove the `composeApp/src/jvmMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: - * the `compose.desktop {}` block, - * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `jvm()` target declaration inside the `kotlin {}` block. +8. To run your desktop app, change and rename the **composeApp [jvm]** run configuration or add a similar one. + In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:desktopApp`. +9. Start the run configuration to make sure that the app runs as expected. +10. If everything works correctly: + * Remove the `composeApp/src/jvmMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `compose.desktop {}` block, + * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `jvm()` target declaration inside the `kotlin {}` block. #### Web app @@ -602,18 +612,19 @@ Create and configure the web app module: If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working and the code is compiling. 6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. -7. Run your web app from the gutter in the `webApp/src/webMain/.../main.kt` file: click the green arrow next - to the `main()` function and select **Run 'webApp [wasmJs]'**. -8. If everything works correctly: - * Remove the `composeApp/src/jvmMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: - * the `compose.desktop {}` block, - * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `jvm()` target declaration inside the `kotlin {}` block. +7. Run your web app: change and rename the **composeApp [wasmJs]** and **composeApp [js]** run configurations or add similar ones. + In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. +8. Start the run configurations to make sure that the app runs as expected. +9. If everything works correctly: + * Remove the `composeApp/src/webMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the web-related code: + * the `webMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `js {}` and `wasmJs {}` target declarations inside the `kotlin {}` block. ### Update the iOS integration -Since the iOS app is not a separate Gradle module, you can simply move the source set into the `sharedUi` directory: +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, `sharedUi` makes most sense: 1. Move the `composeApp/src/iosMain` directory into the `sharedUi/src` directory. 2. Configure the Xcode project to consume the framework produced by the `sharedUi` module: @@ -622,24 +633,39 @@ Since the iOS app is not a separate Gradle module, you can simply move the sourc 3. Find the **Compile Kotlin Framework** phase. 4. Find the line starting with `./gradlew` and swap `composeApp` for `sharedUi`: - ```text - ./gradlew :sharedUi:embedAndSignAppleFrameworkForXcode - ``` + ```text + ./gradlew :sharedUi:embedAndSignAppleFrameworkForXcode + ``` 5. Note that the import in the `ContentView.swift` file will stay the same, because it matches the `baseName` parameter from Gradle configuration of the iOS target, not actual name of the module. If you change the framework name in the `sharedUi/build.gradle.kts` file, you need to change the import directive accordingly. -4. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA +3. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA + +### Remove `composeApp` and update the Android Gradle plugin version -### Remove composeApp and TODO create new run configurations? +When all code is working from correct new modules: -Now that me moved all code outside composeApp, check that there are no dependencies left. Then remove composeApp -entirely. +1. Remove the old module completely: + 1. Remove the `composeApp` dependency from the `settings.gradle.kts` file (the line `include(":composeApp")`). + 2. Remove the `composeApp` directory entirely. + 3. If you followed all instructions, you have working run configurations for the new project structure. + You can remove old run configurations associated with the `composeApp` module. +2. In the `gradle/libs.versions.toml` file, update the AGP version to a 9.* version, for example: + + ```txt + [versions] + agp = "9.0.0" + ``` +3. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + build script editor. +4. That your apps build and run with the new AGP. +Congratulations, you modernized and optimized the structure of your project! -## Anything else? +## What's next -If you already have a `shared` module, then this will be the `sharedLogic` module TODO put this in the sharedLogic part. \ No newline at end of file +TODO up for suggestions here — the first thought is to link platform-specific guidance. \ No newline at end of file From 0658029b46d49c3ea5b3efa6382ddaf90ac80633 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 4 Nov 2025 12:20:54 +0100 Subject: [PATCH 06/20] update: change the focus of the intro a little --- .../multiplatform-project-agp-9-migration.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index f00ec03c..f1da4999 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -1,20 +1,17 @@ [//]: # (title: Migrating a Kotlin Multiplatform project to support AGP 9.0) -Kotlin Multiplatform projects with Android targets which were created using the Android Gradle plugin version earlier -than 9.0 -need to be restructured to upgrade to AGP 9.0. -Several APIs needed for KMP configuration are hidden in AGP 9.0 and eventually are going to be removed. -To solve this in the long term, we recommend updating your project structure to isolate AGP usage to an Android module. +Projects where multiplatform modules configured Android targets using the `com.android.application` plugin +need to be restructured to upgrade to Android Gradle plugin 9.0. +AGP deprecates several APIs, but provides a new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin) +to be used instead. -On top of AGP compatibility, there is an issue of migrating to the -new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin). -In the following guide, we highlight changes related to this as well. +The following guide shows how to swap the plugins and restructure the project at the same time. -> To make your project work with AGP 9.0 in the short term, you can manually enable the hidden APIs. +> To make your project work with AGP 9.0 in the short term, you can manually enable the deprecated APIs. > To do that, in the `gradle.properties` file of your project add this property: > `android.enableLegacyVariantApi=true`. > -> The legacy API is going to be removed completely in AGP 10, make sure you finish the migration before that! +> The legacy APIs are going to be removed completely in AGP 10, make sure you finish the migration before that! > {style="note"} From 0ff0a0b9f2b92026673321d748505b6e65a0eaab Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 5 Nov 2025 09:39:26 +0100 Subject: [PATCH 07/20] fix: preciser --- topics/development/multiplatform-project-agp-9-migration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index f1da4999..5e6350ad 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -2,7 +2,8 @@ Projects where multiplatform modules configured Android targets using the `com.android.application` plugin need to be restructured to upgrade to Android Gradle plugin 9.0. -AGP deprecates several APIs, but provides a new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin) +AGP 9 deprecates several APIs which are used by Kotlin Multiplatform, +but provides a new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin) to be used instead. The following guide shows how to swap the plugins and restructure the project at the same time. From 39952d91e228e435817a727c73d990dc917ffa0a Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 18 Nov 2025 10:26:24 +0100 Subject: [PATCH 08/20] update: new structure, separating the mandatory from the recommended --- .../multiplatform-project-agp-9-migration.md | 138 ++++++++++++------ 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 5e6350ad..5580607d 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -1,59 +1,107 @@ -[//]: # (title: Migrating a Kotlin Multiplatform project to support AGP 9.0) +[//]: # (title: Updating a Kotlin Multiplatform project to support AGP 9) + -Projects where multiplatform modules configured Android targets using the `com.android.application` plugin -need to be restructured to upgrade to Android Gradle plugin 9.0. -AGP 9 deprecates several APIs which are used by Kotlin Multiplatform, -but provides a new [Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin) -to be used instead. +Kotlin Multiplatform projects targeting Android need to migrate to using the new +[Android Gradle Library plugin](https://developer.android.com/kotlin/multiplatform/plugin) +to be able to use Android Gradle plugin version 9.0 and newer. -The following guide shows how to swap the plugins and restructure the project at the same time. +> AGP 9 is supported in IntelliJ IDEA starting with version 2025.3 TODO +> and Android Studio starting with Otter 2025.3 TODO +> +{style="note"} + +## Mandatory changes + +### Migrate to built-in Kotlin + +AGP 9.0 brings built-in Kotlin support that is enabled by default, +so you don't need to enable the Kotlin Android Gradle plugin (`org.jetbrains.kotlin.android`) explicitly anymore. + +If you're using [kapt](https://kotlinlang.org/docs/kapt.html) or custom `kotlinOptions`, +you may need to perform additional migration steps. + +Follow the [migration guide](https://developer.android.com/build/migrate-to-built-in-kotlin) +to make sure you don't miss anything. + +### Migrate to the Android Gradle Library plugin + +Previously, to configure an Android target in a multiplatform module you needed to use +the KMP plugin (`org.jetbrains.kotlin.multiplatform`) +together with either +the Android application (`com.android.application`) or the Android library (`com.android.library`) plugin. + +With AGP 9.0, these plugins stop being compatible with KMP, +and you need to migrate to the new Android Gradle Library plugin built specifically for KMP. > To make your project work with AGP 9.0 in the short term, you can manually enable the deprecated APIs. > To do that, in the `gradle.properties` file of your project add this property: > `android.enableLegacyVariantApi=true`. > -> The legacy APIs are going to be removed completely in AGP 10, make sure you finish the migration before that! +> The legacy APIs are going to be removed completely in AGP 10. Make sure you finish the migration before that! > {style="note"} -## Migration guide +For library migration steps, see the [guide in Android documentation](https://developer.android.com/kotlin/multiplatform/plugin#migrate). + +To migrate an app project, you need to have the Android entry point and the shared code in properly configured separate modules. +There is a [general tutorial for migrating a sample app](#step-by-step-migration-of-a-sample-app), +but the mandatory parts are: +* [Create and configure a shared module]() +* [Create and configure an Android app module]() + +## Recommended changes + +Along with updating your Gradle configuration, we recommend to review the structure of your KMP project. +The general approach is to extract shared code in its own module, and create a separate module for each platform-specific +entry point. -In this guide, we show how to restructure a combined multiplatform module into discrete modules clearly delineating -shared logic, shared UI, and individual entry points. +In terms of [Android modularization approach](https://developer.android.com/topic/modularization), +we recommend creating separate **app modules** for platform-specific entry points +(so, a separate module for Android, web, and JVM) +and **feature modules** for shared code. -The example project is a Compose Multiplatform app that is the result of the [](compose-multiplatform-new-project.md) + + +## Step by step migration of a sample app + +The example project that you will prepare for the migration is a Compose Multiplatform app that is the result of the +[](compose-multiplatform-new-project.md) tutorial. -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) -of the sample project. - - - -The example consists of a single Gradle module (`composeApp`) that contains all the shared code and all of the KMP entry -points. -You will extract shared code and entry points into separate modules to reach two goals: - -* Create a more flexible and scalable project structure that allows managing shared logic, shared UI, and different - entry points - separately. -* Isolate the Android module (that uses the `androidApplication` Gradle plugin) from KMP modules (that use the - `androidLibrary` - Gradle plugin). - -For general modularization advice, -see [Android modularization intro](https://developer.android.com/topic/modularization). -In these terms, you are going to create several **app modules**, for each platform, and shared **feature modules**, for -UI and business logic. - -> If your project is simple enough, it might suffice to combine all shared code (shared logic and UI) in a single -module. -> We'll separate them to illustrate the modularisation pattern. -> -{style="note"} +The example consists of a single Gradle module (`composeApp`) that contains all the shared code and KMP entry +points, +and the `iosApp` project with the iOS-specific code and configuration. + +To prepare for the AGP 9 migration and to create a generally more flexible and scalable project structure, +you will: + +* Create feature modules for shared UI and business logic code. +* Create app modules with entry points for each platform. + +If you are looking to only implement changes mandatory for the AGP 9 migration, +you required steps are: + +* [Create and configure a shared module](#create-shared-modules) +* [Create and configure an Android app module](#android-app) + + + +### Create shared modules + +The rule of thumb for shared modules is: + +* If you implement Compose Multiplatform UI for each of your apps, you only need a single `shared` module that + includes all multiplatform dependencies. +* If at least one of your apps has fully native UI and only should depend on common code for business logic, + create a `sharedLogic` and a `sharedUI` module to separate them. + +The UI in the sample project is fully implemented using Compose Multiplatform, +but to illustrate the more general case we'll show the more complicated structure with `sharedUI` and `sharedLogic` modules. +If you only want a single shared module, skip the shared logic step, jump ahead to the [`sharedUI`](#create-a-shared-ui-module) +section and use it as a blueprint. -### Create a shared logic module +#### 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. @@ -194,9 +242,9 @@ To do that, recreate the source set structure with the folders that contain `Pla The entry points 10. Sync Gradle check that there are no expect-actual errors. --> -### Create a shared UI module +#### Create a shared UI module -Isolate shared code implementing common UI elements in the `sharedUi` module: +Extract shared code implementing common UI elements in the `sharedUi` module: 1. Create the `sharedUi` directory at the root of the project. 2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. @@ -440,7 +488,7 @@ Create and configure a new entry point module for the Android app: 9. Start the run configuration to make sure that the app runs as expected. 10. If everything works correctly: * Remove the `composeApp/src/androidMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * In the `composeApp/build.gradle.kts` file, remove the Android-related code: * the `android {}` block, * the `androidMain.dependencies {}`, * the `androidTarget {}` block inside the `kotlin {}` block. From 181fa6e882f7c6beb3c628eb46e6869a6e2a05f5 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 18 Nov 2025 10:27:37 +0100 Subject: [PATCH 09/20] update: new structure, separating the mandatory from the recommended --- topics/development/multiplatform-project-agp-9-migration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 5580607d..f0fed659 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -46,8 +46,8 @@ For library migration steps, see the [guide in Android documentation](https://de To migrate an app project, you need to have the Android entry point and the shared code in properly configured separate modules. There is a [general tutorial for migrating a sample app](#step-by-step-migration-of-a-sample-app), but the mandatory parts are: -* [Create and configure a shared module]() -* [Create and configure an Android app module]() +* [Create and configure a shared module](#create-shared-modules) +* [Create and configure an Android app module](#android-app) ## Recommended changes @@ -701,7 +701,7 @@ When all code is working from correct new modules: You can remove old run configurations associated with the `composeApp` module. 2. In the `gradle/libs.versions.toml` file, update the AGP version to a 9.* version, for example: - ```txt + ```text [versions] agp = "9.0.0" ``` From dcfee840a8c3f5790228321e3b018efa727587a2 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Tue, 18 Nov 2025 10:36:44 +0100 Subject: [PATCH 10/20] update: toc-title --- mpd.tree | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpd.tree b/mpd.tree index f10d63d9..bfcec26a 100644 --- a/mpd.tree +++ b/mpd.tree @@ -106,7 +106,7 @@ - + From aea09d8f026054d8d64dbbc1a914fdc3bad8ab89 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 19 Nov 2025 12:06:53 +0100 Subject: [PATCH 11/20] update: android app rework --- .../multiplatform-project-agp-9-migration.md | 657 +++++++++--------- 1 file changed, 347 insertions(+), 310 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index f0fed659..09e3bec9 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -87,9 +87,299 @@ you required steps are: following the tutorial will bring you to the new structure already. So we need to save a sample of "how things used to be" and link to it when the wizard update hits.--> +### Create modules for the Android entry point and other app targets + +The only entry point that needs isolating to enable AGP 9 migration is the Android app. +But in a project with several targets enabled, it's more straightforward and transparent to keep all the entry points +on the same level of the project hierarchy. +So below we provide instructions for all targets supported in the sample app. + +#### Android app + +Create and configure a new entry point module for the Android app: + +1. Create the `androidApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":androidApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: + TODO this needs to be removed before actually building with AGP 9 + + ```text + [plugins] + kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + ``` + + 2. In the `androidApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + } + ``` + + 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + 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 + // ... + } + ``` + + 4. To add the necessary dependencies, copy existing dependencies from the `androidMain.dependencies {}` block + of the `composeApp` build script, and add the dependency on the `composeApp` itself. + In this example the end result should look like this: + + ```kotlin + kotlin { + dependencies { + implementation(projects.composeApp) + implementation(libs.androidx.activity.compose) + implementation(compose.uiToolingPreview) + } + } + ``` + + 5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` + file to the `androidApp/build.gradle.kts` file. + 6. Change the configuration from an Android application to an Android library, + since that's what it becomes. In `composeApp/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() + } + + + + 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Copy the `composeApp/src/androidMain` directory into the `androidApp/src/` directory. +6. Rename the `androidApp/src/androidMain` directory into `main`. +7. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working + and the code is compiling. +8. To run your Android app, change and rename the **composeApp** Android run configuration or add a similar one. + In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. +9. Start the run configuration to make sure that the app runs as expected. +10. If everything works correctly, remove the `composeApp/src/androidMain` directory. + +You have extracted the Android entry point to a separate module. +If you don't have any other entry points and want to keep changes to a minimum to upgrade to AGP 9, +you can jump straight to [configuring a shared module](#configure-a-shared-module). + +#### Desktop JVM app + +Create and configure the JVM desktop app module: + +1. Create the `desktopApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":desktopApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `gradle/libs.versions.toml` file, add the Kotlin JVM Gradle plugin to your version catalog: + + ```text + [plugins] + kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + ``` + + 2. In the `desktopApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) + } + ``` + + 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.composeHotReload) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` + + 4. To add the necessary dependencies on other modules, copy existing dependencies from the + `commonMain.dependencies {}` and `jvmMain.dependencies {}` blocks + of the `composeApp` build script. In this example the end result should look like this: + + ```kotlin + kotlin { + dependencies { + implementation(projects.sharedLogic) + implementation(projects.sharedUI) + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + } + } + ``` + + 5. Copy the `compose.desktop {}` block with desktop-specific configuration from the `composeApp/build.gradle.kts` + file to the `desktopApp/build.gradle.kts` file: + + ```kotlin + compose.desktop { + application { + mainClass = "compose.project.demo.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "compose.project.demo" + packageVersion = "1.0.0" + } + } + } + ``` + 6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Move the code: In the `desktopApp/src` directory, create a new `main` directory. +6. Copy the `composeApp/src/jvmMain/kotlin` directory into the `desktopApp/src/main/` directory: + It's important that the package coordinates are aligned with the `compose.desktop {}` configuration. +7. If everything is configured correctly, the imports in the `desktopApp/src/main/.../main.kt` file are working + and the code is compiling. +8. To run your desktop app, change and rename the **composeApp [jvm]** run configuration or add a similar one. + In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:desktopApp`. +9. Start the run configuration to make sure that the app runs as expected. +10. If everything works correctly: + * Remove the `composeApp/src/jvmMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: + * the `compose.desktop {}` block, + * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `jvm()` target declaration inside the `kotlin {}` block. + +#### Web app + +Create and configure the web app module: + +1. Create the `webApp` directory at the root of the project. +2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. +3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: + + ```kotlin + include(":webApp") + ``` +4. Configure the Gradle build script for the new module. + + 1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + } + ``` + + 2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` + + 3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block + in the `webApp/build.gradle.kts` file: + + ```kotlin + kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + } + ``` + + 4. Add the necessary dependencies on other modules: + + ```kotlin + kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.sharedLogic) + // Provides the necessary entry point API + implementation(compose.ui) + } + } + } + ``` + + 5. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. + +5. Copy the entire `composeApp/src/webMain` directory into the `webApp/src` directory. + If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working + and the code is compiling. +6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. +7. Run your web app: change and rename the **composeApp [wasmJs]** and **composeApp [js]** run configurations or add similar ones. + In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. +8. Start the run configurations to make sure that the app runs as expected. +9. If everything works correctly: + * Remove the `composeApp/src/webMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the web-related code: + * the `webMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, + * the `js {}` and `wasmJs {}` target declarations inside the `kotlin {}` block. + + ### Create shared modules -The rule of thumb for shared modules is: +The rule of thumb for the migration of shared modules is: * If you implement Compose Multiplatform UI for each of your apps, you only need a single `shared` module that includes all multiplatform dependencies. @@ -97,9 +387,35 @@ The rule of thumb for shared modules is: create a `sharedLogic` and a `sharedUI` module to separate them. The UI in the sample project is fully implemented using Compose Multiplatform, -but to illustrate the more general case we'll show the more complicated structure with `sharedUI` and `sharedLogic` modules. -If you only want a single shared module, skip the shared logic step, jump ahead to the [`sharedUI`](#create-a-shared-ui-module) -section and use it as a blueprint. +so a single `shared` module makes sense. +But to illustrate the more general case there's an optional section on extracting non-UI code and the changes you +would need to make. TODO link + +#### Configure a `shared` module + +Since you have already extracted all entry points (platform-specific code) out of the `composeApp` module, +you can simply continue using it as the module with common code. +You only need to adjust the Gradle configuration: + +1. In `gradle/libs.versions.toml`, + add the Android Gradle Library plugin to your version catalog: + + ```text + [plugins] + androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } + ``` + +2. In the `composeApp/build.gradle.kts` file, add the plugin the plugins necessary for the shared UI module: + + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) + } + ``` #### Create a shared logic module @@ -155,7 +471,7 @@ Isolate the corresponding code in a `sharedLogic` module: ```kotlin kotlin { // There's no need for iOS framework configuration since sharedLogic - // is not going to be exported as a framework, only sharedUi is. + // is not going to be exported as a framework, only sharedUI is. iosArm64() iosSimulatorArm64() @@ -225,33 +541,16 @@ Isolate the corresponding code in a `sharedLogic` module: You have isolated the shared logic in a separate module and successfully 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: +Extract shared code implementing common UI elements in the `sharedUI` module: -1. Create the `sharedUi` directory at the root of the project. +1. Create the `sharedUI` directory at the root of the project. 2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. 3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: ```kotlin - include(":sharedUi") + include(":sharedUI") ``` 4. Configure the Gradle build script for the new module: @@ -263,7 +562,7 @@ Extract shared code implementing common UI elements in the `sharedUi` module: androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } ``` - 2. In the `sharedUi/build.gradle.kts` file, specify the plugins necessary for the shared UI module: + 2. In the `sharedUI/build.gradle.kts` file, specify the plugins necessary for the shared UI module: ```kotlin plugins { @@ -299,7 +598,7 @@ Extract shared code implementing common UI elements in the `sharedUi` module: iosTarget.binaries.framework { // This is the name of the iOS framework you're going // to import in your Swift code. - baseName = "SharedUi" + baseName = "sharedUI" isStatic = true } } @@ -364,10 +663,10 @@ Extract shared code implementing common UI elements in the `sharedUi` module: ``` 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the editor. -5. Create a new `commonMain/kotlin` directory inside `sharedUi/src`. -6. Move resource files to the `sharedUi` module: the entire directory of `composeApp/commonMain/composeResources` should - be relocated to `sharedUi/commonMain/composeResources`. -7. In the `sharedUi/src/commonMain/kotlin directory`, create a new `App.kt` file. +5. Create a new `commonMain/kotlin` directory inside `sharedUI/src`. +6. Move resource files to the `sharedUI` module: the entire directory of `composeApp/commonMain/composeResources` should + be relocated to `sharedUI/commonMain/composeResources`. +7. In the `sharedUI/src/commonMain/kotlin directory`, create a new `App.kt` file. 8. Copy the entire contents of the original `composeApp/src/commonMain/.../App.kt` to the new `App.kt` file. 9. Comment out all code in the old `App.kt` file in the meantime. You'll test whether the shared UI module is working before removing old code completely. @@ -380,16 +679,16 @@ Extract shared code implementing common UI elements in the `sharedUi` module: import demo.composeapp.generated.resources.mx - import demo.sharedui.generated.resources.mx + import demo.sharedUI.generated.resources.mx 11. Make the new `App()` composable available to the entry poins in the `composeApp` module. - To do that, declare the dependency between `composeApp` and `sharedUi` in the `composeApp/build.gradle.kts` file: + To do that, declare the dependency between `composeApp` and `sharedUI` in the `composeApp/build.gradle.kts` file: ```kotlin commonMain.dependencies { implementation(projects.sharedLogic) - implementation(projects.sharedUi) + implementation(projects.sharedUI) } ``` 12. Run your apps to check that the new module works to supply app entry points with shared UI code. @@ -398,295 +697,27 @@ Extract shared code implementing common UI elements in the `sharedUi` module: You have successfully moved the cross-platform UI code to a dedicated module. The only thing left is to create dedicated modules for every app you are producing with this project. -### Create modules for each app entry point - -As stated in the beginning of this page, the only module that needs isolating is the Android app entry point. -But if you have other targets enabled, it's more straightforward and transparent to keep all the entry points -on the same level of the project hierarchy. - -#### Android app - -Create and configure a new entry point module for the Android app: - -1. Create the `androidApp` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: - - ```kotlin - include(":androidApp") - ``` -4. Configure the Gradle build script for the new module. - - 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: - - ```text - [plugins] - kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - ``` - - 2. In the `androidApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinAndroid) - alias(libs.plugins.androidApplication) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - - 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.kotlinAndroid) apply false - alias(libs.plugins.androidApplication) - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` - - 4. To add the necessary dependencies on other modules, copy existing dependencies from the - `commonMain.dependencies {}` and `androidMain.dependencies {}` blocks - of the `composeApp` build script. In this example the end result should look like this: - - ```kotlin - kotlin { - dependencies { - implementation(projects.sharedLogic) - implementation(projects.sharedUi) - implementation(libs.androidx.activity.compose) - implementation(compose.preview) - } - } - ``` - - 5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` - file to the `androidApp/build.gradle.kts` file. - - 6. In the `kotlin {}` block, add a `compilerOptions {}` block that would match the Java version specified for - the Android application. In our example: - - ```kotlin - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - ``` - - 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Copy the `composeApp/src/androidMain` directory into the `androidApp/src/` directory. -6. Rename the `androidApp/src/androidMain` directory into `main`. -7. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working - and the code is compiling. -8. To run your Android app, change and rename the **composeApp** Android run configuration or add a similar one. - In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. -9. Start the run configuration to make sure that the app runs as expected. -10. If everything works correctly: - * Remove the `composeApp/src/androidMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the Android-related code: - * the `android {}` block, - * the `androidMain.dependencies {}`, - * the `androidTarget {}` block inside the `kotlin {}` block. - -#### Desktop JVM app - -Create and configure the JVM desktop app module: - -1. Create the `desktopApp` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: - - ```kotlin - include(":desktopApp") - ``` -4. Configure the Gradle build script for the new module. - - 1. In the `gradle/libs.versions.toml` file, add the Kotlin JVM Gradle plugin to your version catalog: - - ```text - [plugins] - kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } - ``` - - 2. In the `desktopApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - alias(libs.plugins.composeHotReload) - } - ``` - - 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.kotlinJvm) apply false - alias(libs.plugins.composeHotReload) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` - - 4. To add the necessary dependencies on other modules, copy existing dependencies from the - `commonMain.dependencies {}` and `jvmMain.dependencies {}` blocks - of the `composeApp` build script. In this example the end result should look like this: - - ```kotlin - kotlin { - dependencies { - implementation(projects.sharedLogic) - implementation(projects.sharedUi) - implementation(compose.desktop.currentOs) - implementation(libs.kotlinx.coroutinesSwing) - } - } - ``` - - 5. Copy the `compose.desktop {}` block with desktop-specific configuration from the `composeApp/build.gradle.kts` - file to the `desktopApp/build.gradle.kts` file: - - ```kotlin - compose.desktop { - application { - mainClass = "compose.project.demo.MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "compose.project.demo" - packageVersion = "1.0.0" - } - } - } - ``` - 6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Move the code: In the `desktopApp/src` directory, create a new `main` directory. -6. Copy the `composeApp/src/jvmMain/kotlin` directory into the `desktopApp/src/main/` directory: - It's important that the package coordinates are aligned with the `compose.desktop {}` configuration. -7. If everything is configured correctly, the imports in the `desktopApp/src/main/.../main.kt` file are working - and the code is compiling. -8. To run your desktop app, change and rename the **composeApp [jvm]** run configuration or add a similar one. - In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:desktopApp`. -9. Start the run configuration to make sure that the app runs as expected. -10. If everything works correctly: - * Remove the `composeApp/src/jvmMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: - * the `compose.desktop {}` block, - * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `jvm()` target declaration inside the `kotlin {}` block. - -#### Web app - -Create and configure the web app module: - -1. Create the `webApp` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: - - ```kotlin - include(":webApp") - ``` -4. Configure the Gradle build script for the new module. - - 1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - - 2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` - - 3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block - in the `webApp/build.gradle.kts` file: - - ```kotlin - kotlin { - js { - browser() - binaries.executable() - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - binaries.executable() - } - } - ``` - - 4. Add the necessary dependencies on other modules: - - ```kotlin - kotlin { - sourceSets { - commonMain.dependencies { - implementation(projects.sharedLogic) - // Provides the necessary entry point API - implementation(compose.ui) - } - } - } - ``` - - 5. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Copy the entire `composeApp/src/webMain` directory into the `webApp/src` directory. - If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working - and the code is compiling. -6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. -7. Run your web app: change and rename the **composeApp [wasmJs]** and **composeApp [js]** run configurations or add similar ones. - In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. -8. Start the run configurations to make sure that the app runs as expected. -9. If everything works correctly: - * Remove the `composeApp/src/webMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the web-related code: - * the `webMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `js {}` and `wasmJs {}` target declarations inside the `kotlin {}` block. ### 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, `sharedUi` makes most sense: +In this example, `sharedUI` makes most sense: -1. Move the `composeApp/src/iosMain` directory into the `sharedUi/src` directory. -2. Configure the Xcode project to consume the framework produced by the `sharedUi` module: +1. Move the `composeApp/src/iosMain` directory into the `sharedUI/src` directory. +2. Configure the Xcode project to consume the framework produced by the `sharedUI` module: 1. Select the **File | Open Project in Xcode** menu item. 2. Click the **iosApp** project in the **Project navigator** tool window, then select the **Build Phases** tab. 3. Find the **Compile Kotlin Framework** phase. - 4. Find the line starting with `./gradlew` and swap `composeApp` for `sharedUi`: + 4. Find the line starting with `./gradlew` and swap `composeApp` for `sharedUI`: ```text - ./gradlew :sharedUi:embedAndSignAppleFrameworkForXcode + ./gradlew :sharedUI:embedAndSignAppleFrameworkForXcode ``` 5. Note that the import in the `ContentView.swift` file will stay the same, because it matches the `baseName` parameter from Gradle configuration of the iOS target, not actual name of the module. - If you change the framework name in the `sharedUi/build.gradle.kts` file, you need to change the import directive accordingly. + If you change the framework name in the `sharedUI/build.gradle.kts` file, you need to change the import directive accordingly. 3. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA @@ -705,10 +736,16 @@ When all code is working from correct new modules: [versions] agp = "9.0.0" ``` -3. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the +3. Remove this line from the `androidApp/build.gradle.kts` file, since [Kotlin support is built-in with AGP 9](https://developer.android.com/build/migrate-to-built-in-kotlin) + and the Kotlin Android plugin is no longer necessary: + + ```kotlin + alias(libs.plugins.kotlinAndroid) + ``` +4. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the build script editor. -4. That your apps build and run with the new AGP. +5. That your apps build and run with the new AGP. Congratulations, you modernized and optimized the structure of your project! From 4213748aed19c03b5b8c514b848c0d470cd42602 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Thu, 20 Nov 2025 13:43:04 +0100 Subject: [PATCH 12/20] update: rework for the shared module(s) --- .../multiplatform-project-agp-9-migration.md | 138 ++++++++++-------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 09e3bec9..f18ab13a 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -1,4 +1,4 @@ -[//]: # (title: Updating a Kotlin Multiplatform project to support AGP 9) +[//]: # (title: Recommended project structure with AGP 9 and newer) Kotlin Multiplatform projects targeting Android need to migrate to using the new @@ -19,7 +19,6 @@ so you don't need to enable the Kotlin Android Gradle plugin (`org.jetbrains.kot If you're using [kapt](https://kotlinlang.org/docs/kapt.html) or custom `kotlinOptions`, you may need to perform additional migration steps. - Follow the [migration guide](https://developer.android.com/build/migrate-to-built-in-kotlin) to make sure you don't miss anything. @@ -46,7 +45,7 @@ For library migration steps, see the [guide in Android documentation](https://de To migrate an app project, you need to have the Android entry point and the shared code in properly configured separate modules. There is a [general tutorial for migrating a sample app](#step-by-step-migration-of-a-sample-app), but the mandatory parts are: -* [Create and configure a shared module](#create-shared-modules) +* [Create and configure a shared module](#configure-a-shared-module) * [Create and configure an Android app module](#android-app) ## Recommended changes @@ -80,7 +79,7 @@ you will: If you are looking to only implement changes mandatory for the AGP 9 migration, you required steps are: -* [Create and configure a shared module](#create-shared-modules) +* [Create and configure a shared module](#configure-a-shared-module) * [Create and configure an Android app module](#android-app) - -## Step by step migration of a sample app +## Migration of a sample app The example project that you will prepare for the migration is a Compose Multiplatform app that is the result of the [](compose-multiplatform-new-project.md) -tutorial. TODO link to a branch with this state -The example consists of a single Gradle module (`composeApp`) that contains all the shared code and KMP entry -points, -and the `iosApp` project with the iOS-specific code and configuration. +tutorial. +The sample with the example of an app that needs to be updated is in the [main](https://github.com/kotlin-hands-on/get-started-with-cm/tree/main) +branch of the sample repository. -To prepare for the AGP 9 migration and to create a generally more flexible and scalable project structure, -you will: - -* Create feature modules for shared UI and business logic code. -* Create app modules with entry points for each platform. + -* [Create and configure a shared module](#configure-a-shared-module) -* [Create and configure an Android app module](#android-app) +The sample consists of a single Gradle module (`composeApp`) that contains all the shared code and KMP entry +points, +and the `iosApp` project with the iOS-specific code and configuration. - +To prepare for the AGP 9 migration, you will: -### Create modules for the Android entry point and other app targets +* [Extract the Android app entry point](#android-app) into a separate `androidApp` module. +* [Reconfigure the module with shared code](#configure-the-shared-module-to-use-the-android-kmp-library-plugin) (`composeApp`) to use the Android-KMP library plugin -The only entry point that needs isolating to enable AGP 9 migration is the Android app. -However, if you have multiple targets enabled, it is more straightforward and transparent to keep all the entry points -on the same level of the project hierarchy. -So below we provide instructions for all targets supported in the sample app. +### Module for the Android app entry point {id="android-app"} -#### Android app +#### Create and configure the Android app module -Create and configure a new entry point module for the Android app: +To create a desktop app module (`androidApp`): 1. Create the `androidApp` directory at the root of the project. 2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. @@ -109,288 +79,115 @@ Create and configure a new entry point module for the Android app: ```kotlin include(":androidApp") ``` -4. Configure the Gradle build script for the new module. - - 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: - TODO this needs to be removed before actually building with AGP 9 - ```text - [plugins] - kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - ``` +#### Configure the build script for the Android app - 2. In the `androidApp/build.gradle.kts` file, specify the plugins necessary for the Android app module: +To make the Android app build script work: - ```kotlin - plugins { - alias(libs.plugins.kotlinAndroid) - alias(libs.plugins.androidApplication) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - - 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: +1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: + TODO this needs to be removed before actually building with AGP 9 - ```kotlin - 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 - // ... - } - ``` - - 4. To add the necessary dependencies, copy existing dependencies from the `androidMain.dependencies {}` block - of the `composeApp` build script, and add the dependency on the `composeApp` itself. - In this example the end result should look like this: - - ```kotlin - kotlin { - dependencies { - implementation(projects.composeApp) - implementation(libs.androidx.activity.compose) - implementation(compose.uiToolingPreview) - } - } - ``` - - 5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` - file to the `androidApp/build.gradle.kts` file. - 6. Change the configuration of the `composeApp` module from an Android application to an Android library, - since that's what it effectively becomes. In `composeApp/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() - } - - - - 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Copy the `composeApp/src/androidMain` directory into the `androidApp/src/` directory. -6. Rename the `androidApp/src/androidMain` directory to `main`. -7. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working - and the code is compiling. -8. To run your Android app, modify the **composeApp** Android run configuration or add a similar one. - In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. -9. Start the run configuration to make sure that the app runs as expected. -10. If everything works correctly: - * Delete the `composeApp/src/androidMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the `kotlin.sourceSets.androidMain.dependencies {}` block. - -You have extracted the Android entry point to a separate module! - -If you don't have any other entry points, or want to keep changes to a minimum to upgrade to AGP 9, -jump straight to [configuring a shared module](#configure-a-shared-module). - -#### Desktop JVM app - -Create and configure the JVM desktop app module: + ```text + [plugins] + kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + ``` -1. Create the `desktopApp` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: +2. In the `androidApp/build.gradle.kts` file, specify the plugins necessary for the Android app module: ```kotlin - include(":desktopApp") + plugins { + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + } ``` -4. Configure the Gradle build script for the new module. - - 1. In the `gradle/libs.versions.toml` file, add the Kotlin JVM Gradle plugin to your version catalog: - - ```text - [plugins] - kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } - ``` - - 2. In the `desktopApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - - 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.kotlinJvm) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` - - 4. To add the necessary dependencies on other modules, copy existing dependencies from the - `commonMain.dependencies {}` and `jvmMain.dependencies {}` blocks - of the `composeApp` build script. In this example the end result should look like this: - - ```kotlin - kotlin { - dependencies { - implementation(projects.sharedLogic) - implementation(projects.sharedUI) - implementation(compose.desktop.currentOs) - implementation(libs.kotlinx.coroutinesSwing) - } - } - ``` - - 5. Copy the `compose.desktop {}` block with desktop-specific configuration from the `composeApp/build.gradle.kts` - file to the `desktopApp/build.gradle.kts` file: - - ```kotlin - compose.desktop { - application { - mainClass = "compose.project.demo.MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "compose.project.demo" - packageVersion = "1.0.0" - } - } - } - ``` - 6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Move the code: In the `desktopApp/src` directory, create a new `main` directory. -6. Copy the `composeApp/src/jvmMain/kotlin` directory into the `desktopApp/src/main/` directory: - It's important that the package coordinates are aligned with the `compose.desktop {}` configuration. -7. If everything is configured correctly, the imports in the `desktopApp/src/main/.../main.kt` file are working - and the code is compiling. -8. To run your desktop app, modify and rename the **composeApp [jvm]** run configuration or add a similar one. - In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:desktopApp`. -9. Start the run configuration to make sure that the app runs as expected. -10. If everything works correctly: - * Delete the `composeApp/src/jvmMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the desktop-related code: - * the `compose.desktop {}` block, - * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `jvm()` target declaration inside the `kotlin {}` block. - -#### Web app -Create and configure the web app module: - -1. Create the `webApp` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to project settings in the `settings.gradle.kts` file by adding this line at the end of the file: +3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: ```kotlin - include(":webApp") + 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 + // ... + } ``` -4. Configure the Gradle build script for the new module. - - 1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - 2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: +4. To add the necessary dependencies, copy existing dependencies from the `androidMain.dependencies {}` block + of the `composeApp` build script, and add the dependency on the `composeApp` itself. + In this example the end result should look like this: - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` - - 3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block - in the `webApp/build.gradle.kts` file: - - ```kotlin - kotlin { - js { - browser() - binaries.executable() - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - binaries.executable() - } - } - ``` - - 4. Add the necessary dependencies on other modules: - - ```kotlin - kotlin { - sourceSets { - commonMain.dependencies { - implementation(projects.sharedLogic) - // Provides the necessary entry point API - implementation(compose.ui) - } - } + ```kotlin + kotlin { + dependencies { + implementation(projects.composeApp) + implementation(libs.androidx.activity.compose) + implementation(compose.uiToolingPreview) } - ``` + } + ``` + +5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` + file to the `androidApp/build.gradle.kts` file. +6. Change the configuration of the `composeApp` module from an Android application to an Android library, + since that's what it effectively becomes. In `composeApp/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() + } + + + +7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. - 5. 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 -5. Copy the entire `composeApp/src/webMain` directory into the `webApp/src` directory. - If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working +1. Move the `composeApp/src/androidMain` directory into the `androidApp/src/` directory. +2. Rename the `androidApp/src/androidMain` directory to `main`. +3. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working and the code is compiling. -6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. -7. Run your web app: change and rename the **composeApp [wasmJs]** and **composeApp [js]** run configurations or add similar ones. - In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. -8. Start the run configurations to make sure that the app runs as expected. -9. If everything works correctly: - * Delete the `composeApp/src/webMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the web-related code: - * the `webMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, - * the `js {}` and `wasmJs {}` target declarations inside the `kotlin {}` block. - -### Configure a shared module +4. To run your Android app, modify the **composeApp** Android run configuration: + 1. In the run configuration dropdown, select **Edit Configurations**. + 2. Find the **composeApp** configuration in the **Android** category. + 3. In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. +5. Start the updated run configuration to make sure that the app runs as expected. +6. If everything works correctly: + * Delete the `composeApp/src/androidMain` directory. + * In the `composeApp/build.gradle.kts` file, remove the `kotlin.sourceSets.androidMain.dependencies {}` block. -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: we can simply repurpose `composeApp` as the common code module. +You have extracted the Android entry point to a separate module. +Now let's update the common code module to use the new Android-KMP library plugin. -For an overview of other project configurations and ways of dealing with them, see our blogpost about -the new recommended project structure TODO link +### Configure the shared module to use the Android-KMP library plugin -The only thing you need to adjust in the Gradle configuration that is not covered by sections on app modules -is the new Android Library Gradle plugin: +To simply extract the Android entry point, we used the `com.android.library` plugin for the shared `composeApp` module. +Now let's migrate to the new multiplatform library plugin: 1. In `gradle/libs.versions.toml`, add the Android-KMP library plugin to your version catalog: @@ -436,317 +233,6 @@ is the new Android Library Gradle plugin: delete the `kotlin.sourceSets.androidMain.dependencies {}` block. 7. Check that the Android app is running as expected. -### (Optional) Separate shared logic and shared UI {collapsible="true"} - -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. - -> If your project already has a `shared` module, for example, because you don't share all UI code, -> then you can use this module instead of `sharedLogic`. -> It may be nice to rename it to clearer distinguish shared logic from UI. -> -{style="note"} - -Isolate the corresponding code in a `sharedLogic` module: - -1. Create the `sharedLogic` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: - - ```kotlin - include(":sharedLogic") - ``` -4. Configure the Gradle build script for the new module. - - 1. In the `gradle/libs.versions.toml` file, add the Android-KMP library plugin to your version catalog: - - ```text - [plugins] - androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } - ``` - - 2. In the `sharedLogic/build.gradle.kts` file, specify the plugins necessary for the shared logic module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidMultiplatformLibrary) - } - ``` - 3. Make sure these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - plugins { - alias(libs.plugins.androidMultiplatformLibrary) apply false - alias(libs.plugins.kotlinMultiplatform) apply false - // ... - } - ``` - 4. In the `sharedLogic/build.gradle.kts` file, specify the targets that the common module should support in this example: - - ```kotlin - 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() - } - } - ``` - 5. For Android, instead of the `androidTarget {}` block, add the `androidLibrary {}` configuration to the - `kotlin {}` block: - - ```kotlin - 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 - } - } - } - ``` - 6. Add the necessary time dependencies for the common and JavaScript source sets in the same - way they are declared for `composeApp`: - - ```kotlin - kotlin { - sourceSets { - commonMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-datetime:%dateTimeVersion%") - } - webMain.dependencies { - implementation(npm("@js-joda/timezone", "2.22.0")) - } - } - } - ``` - 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. - -5. Move the business logic code identified in the beginning: - 1. Create a `commonMain/kotlin` directory inside `sharedLogic/src`. - 2. Inside `commonMain/kotlin`, create the `CurrentTime.kt` file. - 3. Move the `currentTimeAt` function from the original `App.kt` to `CurrentTime.kt`. -6. Make the function available to the `App()` composable at its new place. - To do that, declare the dependency between `composeApp` and `sharedLogic` in the `composeApp/build.gradle.kts` file: - - ```kotlin - commonMain.dependencies { - implementation(projects.sharedLogic) - } - ``` -7. Run **Build | Sync Project with Gradle Files** again to apply the changes. -8. In the `composeApp/commonMain/.../App.kt` file, import the `currentTimeAt()` function to fix the code. -9. 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: - -1. Create the `sharedUI` directory at the root of the project. -2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. -3. Add the new module to `settings.gradle.kts` by adding this line at the end of the file: - - ```kotlin - include(":sharedUI") - ``` -4. Configure the Gradle build script for the new module: - - 1. If you haven't done this for the `sharedLogic` module, in `gradle/libs.versions.toml`, - add the Android-KMP library plugin to your version catalog: - - ```text - [plugins] - androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } - ``` - - 2. In the `sharedUI/build.gradle.kts` file, specify the plugins necessary for the shared UI module: - - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` - - 3. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - - ```kotlin - 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 - // ... - } - ``` - - 4. In the `kotlin {}` block, specify the targets that the shared UI module should support in this example: - - ```kotlin - 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() - } - } - ``` - - 5. For Android, instead of the `androidTarget {}` block, add the `androidLibrary {}` configuration to the - `kotlin {}` block: - - ```kotlin - 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 - } - } - } - ``` - - 6. Add the necessary dependencies for the shared UI in the same way they are declared for `composeApp`: - - ```kotlin - 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:%dateTimeVersion%") - } - } - } - ``` - 7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. -5. Create a new `commonMain/kotlin` directory inside `sharedUI/src`. -6. Move resource files to the `sharedUI` module: the entire directory of `composeApp/commonMain/composeResources` should - be relocated to `sharedUI/commonMain/composeResources`. -7. In the `sharedUI/src/commonMain/kotlin directory`, create a new `App.kt` file. -8. Copy the entire contents of the original `composeApp/src/commonMain/.../App.kt` to the new `App.kt` file. -9. Temporarily comment out all code in the old `App.kt` file. - This will allow you to test whether the shared UI module is working before removing the old code completely. -10. The new `App.kt` file should work as expected, except for resource imports, which are now located in a - different package. - Reimport the `Res` object and all drawable resources with the correct path, for example: - - - - import demo.composeapp.generated.resources.mx - - - import demo.sharedui.generated.resources.mx - - -11. To make the new `App()` composable available to the entry points in your app modules that rely on it, - add the dependency to the corresponding `build.gradle.kts` files: - - ```kotlin - kotlin { - sourceSets { - commonMain.dependencies { - implementation(projects.sharedUI) - // ... - } - } - } - ``` -12. Run your apps to check that the new module works to supply app entry points with shared UI code. -13. Remove the `composeApp/src/commonMain/.../App.kt` file. - -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`: - -1. Move the `composeApp/src/iosMain` directory into the `shared/src` directory. -2. Configure the Xcode project to consume the framework produced by the `shared` module: - 1. Select the **File | Open Project in Xcode** menu item. - 2. Click the **iosApp** project in the **Project navigator** tool window, then select the **Build Phases** tab. - 3. Find the **Compile Kotlin Framework** phase. - 4. Find the line starting with `./gradlew` and replace `composeApp` with `sharedUi`: - - ```text - ./gradlew :shared:embedAndSignAppleFrameworkForXcode - ``` - - 5. Note that the import in the `ContentView.swift` file needs to stay the same, because it matches the `baseName` parameter from - Gradle configuration of the iOS target, - not actual name of the module. - If you change the framework name in the `shared/build.gradle.kts` file, you need to change the import directive accordingly. - -3. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA - ### Update the Android Gradle plugin version When all code is working with the new configuration: @@ -757,7 +243,7 @@ When all code is working with the new configuration: ```text [versions] - agp = "9.0.0" + agp = "9.0.0-beta02" ``` 3. Remove this line from the `androidApp/build.gradle.kts` file, since [Kotlin support is built-in with AGP 9](https://developer.android.com/build/migrate-to-built-in-kotlin) and applying the Kotlin Android plugin is no longer necessary: @@ -774,4 +260,5 @@ Congratulations! You have modernized and optimized the structure of your project ## What's next -TODO up for suggestions here — the first thought is to link platform-specific guidance. \ No newline at end of file +Check out the [recommended project structure](multiplatform-project-recommended-structure.md) +which follows the logic of separating entry points for any app target you might have. \ No newline at end of file diff --git a/topics/development/multiplatform-project-recommended-structure.md b/topics/development/multiplatform-project-recommended-structure.md index 871b29fc..b9e4ae9d 100644 --- a/topics/development/multiplatform-project-recommended-structure.md +++ b/topics/development/multiplatform-project-recommended-structure.md @@ -47,6 +47,8 @@ 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. + + ### Desktop JVM app #### Create and configure the desktop app module @@ -127,7 +129,7 @@ To make the desktop app build script work: 6. 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 app +#### Move the code and run the desktop app After the configuration is complete, move the code of the desktop app to the new directory: @@ -148,9 +150,11 @@ After the configuration is complete, move the code of the desktop app to the new * the `jvmMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, * the `jvm()` target declaration inside the `kotlin {}` block. -#### Web app +### Web app + +#### Create and configure the web app module -Create and configure the web app module: +To create a desktop app module (`webApp`): 1. Create the `webApp` directory at the root of the project. 2. Inside that directory, create an empty `build.gradle.kts` file and the `src` directory. @@ -159,9 +163,12 @@ Create and configure the web app module: ```kotlin include(":webApp") ``` -4. Configure the Gradle build script for the new module. - 1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: +#### Configure the build script for the web app + +To make the desktop app build script work: + +1. In the `webApp/build.gradle.kts` file, specify the plugins necessary for the shared UI module: ```kotlin plugins { @@ -171,75 +178,85 @@ Create and configure the web app module: } ``` - 2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: +2. Make sure all of these plugins are mentioned in the **root** `build.gradle.kts` file: - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - // ... - } - ``` + ```kotlin + plugins { + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + // ... + } + ``` - 3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block - in the `webApp/build.gradle.kts` file: +3. Copy the JavaScript and Wasm target declarations from the `composeApp/build.gradle.kts` file into the `kotlin {}` block + in the `webApp/build.gradle.kts` file: - ```kotlin - kotlin { - js { - browser() - binaries.executable() - } + ```kotlin + kotlin { + js { + browser() + binaries.executable() + } - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - binaries.executable() - } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() } - ``` + } + ``` - 4. Add the necessary dependencies on other modules: +4. Add the necessary dependencies on other modules: - ```kotlin - kotlin { - sourceSets { - commonMain.dependencies { - implementation(projects.sharedLogic) - // Provides the necessary entry point API - implementation(compose.ui) - } + ```kotlin + kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.sharedLogic) + // Provides the necessary entry point API + implementation(compose.ui) } } - ``` + } + ``` - 5. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - editor. +5. 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 -5. Copy the entire `composeApp/src/webMain` directory into the `webApp/src` directory. +After the configuration is complete, move the code of the web app to the new directory: + +1. Move the entire `composeApp/src/webMain` directory into the `webApp/src` directory. If everything is configured correctly, the imports in the `webApp/src/webMain/.../main.kt` file are working and the code is compiling. -6. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. -7. Run your web app: change and rename the **composeApp [wasmJs]** and **composeApp [js]** run configurations or add similar ones. - In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. -8. Start the run configurations to make sure that the app runs as expected. -9. If everything works correctly: +2. In the `webApp/src/webMain/resources/index.html` file update the script name: from `composeApp.js` to `webApp.js`. +3. To run your web app, modify the **composeApp [wasmJs]** run configuration: + 1. In the run configuration dropdown, select **Edit Configurations**. + 2. Find the **composeApp [wasmJs]** configuration in the **Gradle** category. + 3. In the **Gradle project** field, change `ComposeDemo:composeApp` to `ComposeDemo:webApp`. +4. Repeat for **composeApp [js]** to be able to run the JavaScript version, too. +5. Start the run configurations to make sure that the app runs as expected. +6. If everything works correctly: * Delete the `composeApp/src/webMain` directory. * In the `composeApp/build.gradle.kts` file, remove the web-related code: * the `webMain.dependencies {}` block inside the Kotlin `sourceSets {}` block, * the `js {}` and `wasmJs {}` target declarations inside the `kotlin {}` block. -### Configure a shared module +### 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: we can simply repurpose `composeApp` as the common code module. +to hold all common code: you can simply repurpose `composeApp` as the common code module. For an overview of other project configurations and ways of dealing with them, see our blogpost about the new recommended project structure TODO link -The only thing you need to adjust in the Gradle configuration that is not covered by sections on app modules -is the new Android Library Gradle plugin: +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: 1. In `gradle/libs.versions.toml`, add the Android-KMP library plugin to your version catalog: @@ -596,31 +613,6 @@ In this example, you can leave it inside `shared`: 3. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA -### Update the Android Gradle plugin version - -When all code is working with the new configuration: - -1. If you followed the instructions, you have working run configurations for the new app modules. - You can delete obsolete run configurations associated with the `composeApp` module. -2. In the `gradle/libs.versions.toml` file, update the AGP version to a 9.* version, for example: - - ```text - [versions] - agp = "9.0.0" - ``` -3. Remove this line from the `androidApp/build.gradle.kts` file, since [Kotlin support is built-in with AGP 9](https://developer.android.com/build/migrate-to-built-in-kotlin) - and applying the Kotlin Android plugin is no longer necessary: - - ```kotlin - alias(libs.plugins.kotlinAndroid) - ``` -4. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the - build script editor. - -5. That your apps build and run with the new AGP version. - -Congratulations! You have modernized and optimized the structure of your project. - ## What's next TODO up for suggestions here — the first thought is to link platform-specific guidance. \ No newline at end of file From 5586880f6e0ccab3c2606e35adfa0b74432ffd01 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 26 Nov 2025 11:55:40 +0100 Subject: [PATCH 16/20] updated the Android part, included the app section in the general recommendations --- topics/development/multiplatform-project-agp-9-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index a164c117..8aca1b2d 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -40,7 +40,7 @@ For library migration steps, see the [guide in Android documentation](https://de To migrate an Android app project, you need to have the Android entry point and the shared code in properly configured separate modules. Below is a general tutorial for migrating a sample app, where you can see: * [How to extract the Android app entry point into a separate module](#android-app) -* [How to update the configuration of a shared module](#configure-a-shared-module) +* [How to update the configuration of a shared module](#configure-the-shared-module-to-use-the-android-kmp-library-plugin) ## Migration of a sample app From 727d65845c2b1a6c358128275f6d6d83dbcbfe54 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 3 Dec 2025 01:16:25 +0100 Subject: [PATCH 17/20] update: once again into the breach * tested with AGP 9 in AS Canary * updated to account for friction points --- .../multiplatform-project-agp-9-migration.md | 95 +++++++++++++------ 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 8aca1b2d..cb382dc8 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -1,8 +1,8 @@ [//]: # (title: Updating multiplatform projects with Android apps to use AGP 9) -With AGP 9, the Kotlin Multiplatform Gradle plugin stops being compatible with the `com.android.application` -and the `com.android.library` plugins. +When used along with Android Gradly Plugin 9 or newer, +the Kotlin Multiplatform Gradle plugin stops being compatible with the `com.android.application` and the `com.android.library` plugins. To update your project: * If your Android entry point is currently implemented in a shared code module, @@ -79,10 +79,12 @@ To create a desktop app module (`androidApp`): ```kotlin include(":androidApp") ``` +4. 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 -To make the Android app build script work: +Configure the Gradle build script for the new module: 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: TODO this needs to be removed before actually building with AGP 9 @@ -124,14 +126,27 @@ To make the Android app build script work: dependencies { implementation(projects.composeApp) implementation(libs.androidx.activity.compose) - implementation(compose.uiToolingPreview) + implementation(compose.components.uiToolingPreview) } } ``` 5. Copy the entire `android {}` block with Android-specific configuration from the `composeApp/build.gradle.kts` file to the `androidApp/build.gradle.kts` file. -6. Change the configuration of the `composeApp` module from an Android application to an Android library, +6. To avoid the `Inconsistent JVM Target Compatibility Between Java and Kotlin Tasks` Gradle error, make sure that + your Gradle JVM is in sync with the Java version specified in the `android.compileOptions {}` block. + For example, if your Gradle JVM is set to JetBrains Runtime 21, adjust the version accordingly in the `androidApp/build.gradle.kts` file: + + ```kotlin + android { + // ... + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + ``` +7. Change the configuration of the `composeApp` module from an Android application to an Android library, since that's what it effectively becomes. In `composeApp/build.gradle.kts`: * Change the reference to the Gradle plugin: @@ -163,7 +178,7 @@ To make the Android app build script work: -7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the +8. 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 @@ -172,22 +187,22 @@ To make the Android app build script work: 2. Rename the `androidApp/src/androidMain` directory to `main`. 3. If everything is configured correctly, the imports in the `androidApp/src/main/.../MainActivity.kt` file are working and the code is compiling. -4. To run your Android app, modify the **composeApp** Android run configuration: +4. 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: 1. In the run configuration dropdown, select **Edit Configurations**. 2. Find the **composeApp** configuration in the **Android** category. 3. In the **General | Module** field, change `demo.composeApp` to `demo.androidApp`. -5. Start the updated run configuration to make sure that the app runs as expected. -6. If everything works correctly: - * Delete the `composeApp/src/androidMain` directory. - * In the `composeApp/build.gradle.kts` file, remove the `kotlin.sourceSets.androidMain.dependencies {}` block. +5. Start the new run configuration to make sure that the app runs as expected. +6. If everything works correctly, in the `composeApp/build.gradle.kts` file, remove the `kotlin.sourceSets.androidMain.dependencies {}` block. You have extracted the Android entry point to a separate module. -Now let's update the common code module to use the new Android-KMP library plugin. +Now update the common code module to use the new Android-KMP library plugin. ### Configure the shared module to use the Android-KMP library plugin -To simply extract the Android entry point, we used the `com.android.library` plugin for the shared `composeApp` module. -Now let's migrate to the new multiplatform library plugin: +To simply extract the Android entry point, you applied the `com.android.library` plugin for the shared `composeApp` module. +Now migrate to the new multiplatform library plugin: 1. In `gradle/libs.versions.toml`, add the Android-KMP library plugin to your version catalog: @@ -197,16 +212,16 @@ Now let's migrate to the new multiplatform library plugin: androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } ``` -2. In the `composeApp/build.gradle.kts` file, add the plugin the plugins necessary for the shared UI module: +2. In the `composeApp/build.gradle.kts` file, swap the old Android library plugin for the new one: - ```kotlin - plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - } - ``` + + + alias(libs.plugins.androidLibrary) + + + alias(libs.plugins.androidMultiplatformLibrary) + + 3. In the root `build.gradle.kts` file, add the following line to avoid conflicts in applying the plugin: ```kotlin @@ -228,10 +243,15 @@ Now let's migrate to the new multiplatform library plugin: } } ``` -5. Remove the root `android {}` block from the `composeApp/build.gradle.kts` file. +5. Remove both the `android {}` and the `dependencies {}` block from the `composeApp/build.gradle.kts` file. + * The `android {}` block is replaced with the `kotlin.androidLibrary {}` configuration, + * The only root dependency (`debugImplementation(compose.uiTooling)`) conflicts with the new plugin that doesn't + support build variants. 6. Remove the `androidMain` dependencies, since all code was moved to the app module: delete the `kotlin.sourceSets.androidMain.dependencies {}` block. -7. Check that the Android app is running as expected. +7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the + editor. +8. Check that the Android app is running as expected. ### Update the Android Gradle plugin version @@ -239,22 +259,37 @@ When all code is working with the new configuration: 1. If you followed the instructions, you have working run configurations for the new app modules. You can delete obsolete run configurations associated with the `composeApp` module. -2. In the `gradle/libs.versions.toml` file, update the AGP version to a 9.* version, for example: +2. In the `gradle/libs.versions.toml` file, update the AGP to a 9.* version, for example: ```text [versions] - agp = "9.0.0-beta02" + agp = "9.0.0-beta03" + ``` +3. Up the Gradle version in the `gradle/wrapper/gradle-wrapper.properties` file to at least 9.1.0: + + ```text + distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip ``` -3. Remove this line from the `androidApp/build.gradle.kts` file, since [Kotlin support is built-in with AGP 9](https://developer.android.com/build/migrate-to-built-in-kotlin) +4. Remove this line from the `androidApp/build.gradle.kts` file, since [Kotlin support is built-in with AGP 9](https://developer.android.com/build/migrate-to-built-in-kotlin) and applying the Kotlin Android plugin is no longer necessary: ```kotlin alias(libs.plugins.kotlinAndroid) ``` -4. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the +5. In the `composeApp/build.gradle.kts` file update the namespace in the `kotlin.androidLibrary {}` block + so that it doesn't conflict with the app's namespace, for example: + + ```kotlin + kotlin { + androidLibrary { + namespace = "compose.project.demo.composedemolibrary" + // ... + ``` + +6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the build script editor. -5. That your apps build and run with the new AGP version. +7. That your apps build and run with the new AGP version. Congratulations! You have modernized and optimized the structure of your project. From e475541736ced2910a952981998af5891b7dbfdd Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 3 Dec 2025 05:20:32 +0100 Subject: [PATCH 18/20] fix: leftover things --- topics/development/multiplatform-project-agp-9-migration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index cb382dc8..5c21cf0b 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -87,7 +87,6 @@ To create a desktop app module (`androidApp`): Configure the Gradle build script for the new module: 1. In the `gradle/libs.versions.toml` file, add the Kotlin Android Gradle plugin to your version catalog: - TODO this needs to be removed before actually building with AGP 9 ```text [plugins] @@ -291,7 +290,7 @@ When all code is working with the new configuration: 7. That your apps build and run with the new AGP version. -Congratulations! You have modernized and optimized the structure of your project. +Congratulations! You have upgraded your project to be compatible with AGP 9. ## What's next From 7f9836459761dd95218bce5283fb27c527e1f676 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 3 Dec 2025 05:33:21 +0100 Subject: [PATCH 19/20] fix: leftover things --- topics/development/multiplatform-project-agp-9-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index 5c21cf0b..ea4976a3 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -243,7 +243,7 @@ Now migrate to the new multiplatform library plugin: } ``` 5. Remove both the `android {}` and the `dependencies {}` block from the `composeApp/build.gradle.kts` file. - * The `android {}` block is replaced with the `kotlin.androidLibrary {}` configuration, + * The `android {}` block is replaced with the `kotlin.androidLibrary {}` configuration. * The only root dependency (`debugImplementation(compose.uiTooling)`) conflicts with the new plugin that doesn't support build variants. 6. Remove the `androidMain` dependencies, since all code was moved to the app module: @@ -288,7 +288,7 @@ When all code is working with the new configuration: 6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the build script editor. -7. That your apps build and run with the new AGP version. +7. Check that your app builds and runs with the new AGP version. Congratulations! You have upgraded your project to be compatible with AGP 9. From dacf4386a4b07510f3624b1e55e07431f6550846 Mon Sep 17 00:00:00 2001 From: Aleksey Zamulla Date: Wed, 3 Dec 2025 10:22:37 +0100 Subject: [PATCH 20/20] fix: unnecessary step --- topics/development/multiplatform-project-agp-9-migration.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/topics/development/multiplatform-project-agp-9-migration.md b/topics/development/multiplatform-project-agp-9-migration.md index ea4976a3..1b58e1ef 100644 --- a/topics/development/multiplatform-project-agp-9-migration.md +++ b/topics/development/multiplatform-project-agp-9-migration.md @@ -246,11 +246,9 @@ Now migrate to the new multiplatform library plugin: * The `android {}` block is replaced with the `kotlin.androidLibrary {}` configuration. * The only root dependency (`debugImplementation(compose.uiTooling)`) conflicts with the new plugin that doesn't support build variants. -6. Remove the `androidMain` dependencies, since all code was moved to the app module: - delete the `kotlin.sourceSets.androidMain.dependencies {}` block. -7. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the +6. Select **Build | Sync Project with Gradle Files** in the main menu, or click the Gradle refresh button in the editor. -8. Check that the Android app is running as expected. +7. Check that the Android app is running as expected. ### Update the Android Gradle plugin version