diff --git a/mpd.tree b/mpd.tree index 50725372..4af33b2e 100644 --- a/mpd.tree +++ b/mpd.tree @@ -101,6 +101,7 @@ + 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 + 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 + +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. + +> 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. +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 Gradle 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 `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() + + 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. 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 { + 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 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) + } + ``` + + 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: + +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. + +3. Run the app from Xcode or using the **iosApp** run configuration in IntelliJ IDEA + +### Remove `composeApp` and update the Android Gradle plugin version + +When all code is working from correct new modules: + +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! + +## What's next + +TODO up for suggestions here — the first thought is to link platform-specific guidance. \ No newline at end of file