diff --git a/mpd.tree b/mpd.tree index 8b91c094..066f8723 100644 --- a/mpd.tree +++ b/mpd.tree @@ -130,6 +130,7 @@ + diff --git a/topics/compose-onboard/compose-multiplatform-create-first-app.md b/topics/compose-onboard/compose-multiplatform-create-first-app.md index 70e94a38..295093c6 100644 --- a/topics/compose-onboard/compose-multiplatform-create-first-app.md +++ b/topics/compose-onboard/compose-multiplatform-create-first-app.md @@ -71,39 +71,42 @@ If you didn't select iOS in the wizard, you won't have the folders whose names b The project contains two modules: -* _composeApp_ is a Kotlin module that contains the logic shared among the Android, desktop, iOS, and web applications – the code +* _shared_ is a Kotlin module that contains the logic shared among the Android, desktop, iOS, and web applications – the code you use for all the platforms. It uses [Gradle](https://kotlinlang.org/docs/gradle.html) as the build system that helps you automate your build process. +* _androidApp_ is the module that builds into an Android application. * _iosApp_ is an Xcode project that builds into an iOS application. It depends on and uses the shared module as an iOS framework. ![Compose Multiplatform project structure](compose-project-structure.png) +* _desktopApp_ is the module that builds into a desktop JVM application. It depends on the `shared` module. +* _webApp_ is the module that builds into web applications, both Kotlin/JS and Kotlin/Wasm. + +The **shared** module contains the following source sets: `androidMain`, `commonMain`, `iosMain`, `jsMain`, +`jvmMain`, and `wasmJsMain` (with `-Test` companion source sets if you chose to include tests). -The **composeApp** module consists of the following source sets: `androidMain`, `commonMain`, `iosMain`, `jsMain`, -`jvmMain`, `wasmJsMain`, and `webMain` (with `commonTest` if you chose to include tests). A _source set_ is a Gradle concept for a number of files logically grouped together, where each group has its own dependencies. In Kotlin Multiplatform, different source sets can target different platforms. The `commonMain` source set uses the common Kotlin code, and platform source sets use Kotlin code specific to each -target: +target: * `jvmMain` is the source file for desktop, which uses Kotlin/JVM. * `androidMain` also uses Kotlin/JVM. * `iosMain` uses Kotlin/Native. * `jsMain` uses Kotlin/JS. * `wasmJsMain` uses Kotlin/Wasm. -* `webMain` is the web [intermediate source set](multiplatform-hierarchy.md#manual-configuration) that includes `jsMain` and `wasmJsMain`. -When the shared module is built into an Android library, common Kotlin code gets treated as Kotlin/JVM. When it is built +When the `shared` module is built into an Android library, common Kotlin code gets treated as Kotlin/JVM. When it is built into an iOS framework, common Kotlin code gets treated as Kotlin/Native. When the shared module is built into a web app, common -Kotlin code can be treated as Kotlin/Wasm and Kotlin/JS. +Kotlin code can be treated as Kotlin/Wasm or Kotlin/JS. ![Common Kotlin, Kotlin/JVM, and Kotlin/Native](module-structure.svg){width=700} In general, write your implementation as common code whenever possible instead of duplicating functionality in platform-specific source sets. -In the `composeApp/src/commonMain/kotlin` directory, open the `App.kt` file. It contains the `App()` function, which implements a +In the `shared/src/commonMain/kotlin` directory, open the `App.kt` file. It contains the `App()` function, which implements a minimalistic but complete Compose Multiplatform UI: ```kotlin @@ -129,14 +132,14 @@ fun App() { horizontalAlignment = Alignment.CenterHorizontally, ) { Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") + Text("Compose: ${greeting}") } } } } } ``` -{initial-collapse-state="collapsed" collapsible="true" collapsed-title="fun App()"} +{id="common-app-composable"} Let's run the application on all supported platforms. @@ -155,7 +158,7 @@ order, so start with whichever platform you are most familiar with. 1. In the list of run configurations, select **composeApp**. 2. Choose your Android virtual device and then click **Run**: Your IDE starts the selected virtual device if it - is powered down, and runs the app. + is powered down and runs the app. ![Run the Compose Multiplatform app on Android](compose-run-android.png){width=350} @@ -265,7 +268,7 @@ in IntelliJ IDEA and select your device in the **Execution target** list. Run th ### Run your application on desktop -Select **composeApp [desktop]** in the list of run configurations and click **Run**. By default, the run configuration +Select **desktopApp** (TODO verify) in the list of run configurations and click **Run**. By default, the run configuration starts a desktop app in its own OS window: ![Run the Compose Multiplatform app on desktop](compose-run-desktop.png){width=350} @@ -276,8 +279,8 @@ starts a desktop app in its own OS window: 1. In the list of run configurations, select: - * **composeApp[js]**: To run your Kotlin/JS application. - * **composeApp[wasmJs]**: To run your Kotlin/Wasm application. + * **webApp[js]**: To run your Kotlin/JS application. + * **webApp[wasmJs]**: To run your Kotlin/Wasm application. ![Run the Compose Multiplatform app on web](web-run-configuration.png){width=400} diff --git a/topics/compose-onboard/compose-multiplatform-explore-composables.md b/topics/compose-onboard/compose-multiplatform-explore-composables.md index a2cbb880..350dc634 100644 --- a/topics/compose-onboard/compose-multiplatform-explore-composables.md +++ b/topics/compose-onboard/compose-multiplatform-explore-composables.md @@ -20,38 +20,9 @@ platform-specific code that launches this UI on each platform. ## Implementing composable functions -In the `composeApp/src/commonMain/kotlin/App.kt` file, take a look at the `App()` function: +In the `shared/src/commonMain/kotlin/App.kt` file, take a look at the `App()` function: -```kotlin -@Composable -@Preview -fun App() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.primaryContainer) - .safeContentPadding() - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") - } - } - } - } -} -``` + The `App()` function is a regular Kotlin function annotated with `@Composable`. These kinds of functions are referred to as _composable @@ -65,15 +36,17 @@ A composable function has the following general structure: the `AnimatedVisibility` composable. * The `Button` contains the `Text` composable, which renders some text. * The `AnimatedVisibility` shows and hides the `Image` using an animation. -* The `painterResource` loads a vector icon stored in an XML resource. +* The `painterResource` loads a vector icon stored as an XML file. The `horizontalAlignment` parameter of the `Column` centers its content. But for this to have any effect, the column should take up the full width of its container. This is achieved using the `modifier` parameter. -Modifiers are a key component of Compose Multiplatform. This is a primary mechanism you use to adjust the appearance or -behavior of composables in the UI. Modifiers are created using methods of the `Modifier` type. When you chain these +Modifiers are a key component of Jetpack Compose and Compose Multiplatform. +This is the primary mechanism you use to adjust the appearance or behavior of composables in the UI. +Modifiers are created using methods of the `Modifier` type. When you chain these methods, each call can change the `Modifier` returned from the previous call, making the order significant. -See the [JetPack Compose documentation](https://developer.android.com/jetpack/compose/modifiers) for more details. +See the [Compose Multiplatform introduction to modifiers](https://kotlinlang.org/docs/multiplatform/compose-layout-modifiers.html#built-in-modifiers) +and the extensive [Jetpack Compose modifier documentation](https://developer.android.com/jetpack/compose/modifiers) for more details. ### Managing the state @@ -102,7 +75,7 @@ controller; on the desktop, by a window; and on the web, by a container. Let's e ### On Android -For Android, open the `MainActivity.kt` file in `composeApp/src/androidMain/kotlin`: +For Android, open the `MainActivity.kt` file within `androidApp/src/main/kotlin`: ```kotlin class MainActivity : ComponentActivity() { @@ -122,38 +95,42 @@ called `MainActivity` that invokes the `App` composable. ### On iOS -For iOS, open the `MainViewController.kt` file in `composeApp/src/iosMain/kotlin`: +For iOS, open the `MainViewController.kt` file within `shared/src/iosMain/kotlin`: ```kotlin fun MainViewController() = ComposeUIViewController { App() } ``` This is a [view controller](https://developer.apple.com/documentation/uikit/view_controllers) that performs the same -role as an activity on Android. Notice that both the iOS and Android types simply invoke the `App` composable. +role as an activity on Android. Notice that both the iOS and Android types simply invoke the `App` composable from common code. ### On desktop -For desktop, look at the `main()` function in `composeApp/src/jvmMain/kotlin`: +For desktop, look for the `main.kt` file in `desktopApp/src/main/kotlin`: ```kotlin fun main() = application { - Window(onCloseRequest = ::exitApplication, title = "ComposeDemo") { + Window( + onCloseRequest = ::exitApplication, + title = "ComposeDemo" + ) { App() } } ``` * Here, the `application()` function launches a new desktop application. -* This function takes a lambda, where you initialize the UI. Typically, you create a `Window` and specify properties and - instructions that dictate how the program should react when the window is closed. In this case, the whole application shuts down. -* Inside this window, you can place your content. As with Android and iOS, the only content is the `App()` function. +* This function takes a lambda, which initializes the UI. Typically, you create a `Window` and specify properties and + instructions that dictate how the program should react when the window is closed (`onCloseRequest`). + In this case, the whole application shuts down. +* Inside this window, you can place your content. As with Android and iOS, the only content is the UI provided by the `App()` function. -Currently, the `App` function doesn't declare any parameters. In a larger application, you typically pass parameters to -platform-specific dependencies. These dependencies could be created by hand or using a dependency injection library. +In this example, the `App()` function doesn't declare any parameters. In a larger application, you typically pass parameters to +platform-specific dependencies. These dependencies could be written manually or passed using a dependency injection library. ### On web -In the `composeApp/src/webMain/kotlin/main.kt` file, take a look at the `main()` function: +In the `main.kt` file within the `webApp/src/webMain/kotlin/` directory, take a look at the `main()` function: ```kotlin @OptIn(ExperimentalComposeUiApi::class) @@ -164,15 +141,12 @@ fun main() { } ``` -* The `@OptIn(ExperimentalComposeUiApi::class)` annotation tells the compiler that you are using an API marked as +* The `@OptIn(ExperimentalComposeUiApi::class)` annotation tells the compiler that you are using a Compose API marked as experimental and may change in future releases. * The `ComposeViewport{}` function sets up the Compose environment for the application. * The web app is inserted into the container specified as a parameter for the `ComposeViewport` function. * The `App()` function is responsible for building the UI components of your application using Jetpack Compose. -The `main.kt` file is located -in the `webMain` directory, which contains common code for the web targets. - ## Next step In the next part of the tutorial, you'll add a dependency to the project and modify the user interface. diff --git a/topics/compose-onboard/compose-multiplatform-modify-project.md b/topics/compose-onboard/compose-multiplatform-modify-project.md index 07a048df..61c102ff 100644 --- a/topics/compose-onboard/compose-multiplatform-modify-project.md +++ b/topics/compose-onboard/compose-multiplatform-modify-project.md @@ -25,18 +25,15 @@ But we recommend that you use this approach only when there's no Kotlin Multipla you can rely on the [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) library. > You can explore Kotlin Multiplatform libraries available for your target platforms on [klibs.io](https://klibs.io/), -> an experimental search service from JetBrains for discovering multiplatform libraries. +> a search service by JetBrains for discovering multiplatform libraries. > {style="tip"} To use the `kotlinx-datetime` library: -1. Open the `composeApp/build.gradle.kts` file and add the dependencies to the project: - - * Add the main `kotlinx-datetime` dependency to the section that configures the common code source set. - For simplicity, you can include the version number directly instead of adding it to the version catalog. - * For the web target, timezone support requires the `js-joda` library. - Add a reference to the `js-joda` npm package in the `webMain` dependencies. +1. Open the `shared/build.gradle.kts` file and add the main `kotlinx-datetime` dependency to the section that configures + the common code source set. + For simplicity, you can include the version number directly instead of adding it to the version catalog. ```kotlin kotlin { @@ -47,25 +44,39 @@ To use the `kotlinx-datetime` library: // ... implementation("org.jetbrains.kotlinx:kotlinx-datetime:%dateTimeVersion%") } - webMain.dependencies { + } + } + + ``` +2. For the web target, timezone support requires the `js-joda` library. + Add a reference to the `js-joda` npm package to the `webApp/build.gradle.kts file`: + + ```kotlin + kotlin { + // ... + sourceSets { + // ... + commonMain.dependencies { + // ... implementation(npm("@js-joda/timezone", "2.22.0")) } } } ``` - -2. Once the dependency is added, you're prompted to resync the project. Click the **Sync Gradle Changes** button to synchronize Gradle files: ![Synchronize Gradle files](gradle-sync.png){width=50} -3. In the **Terminal** tool window, run the following command: + Adding the dependency to the `commonMain` source set makes the library available both to the `wasmJs` and `js` targets. + +3. Once the dependency is added, accept the IDE suggestion to sync the Gradle configuration + or press double **Shift** and execute the **Sync Project with Gradle Files** command. + +4. In the **Terminal** tool window, run the following command to ensure that the `yarn.lock` file is updated with the latest dependency versions: ```shell ./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock ``` - - This Gradle task ensures that the `yarn.lock` file is updated with the latest dependency versions. -4. In the `webMain` source set, use the `@JsModule` annotation to import the `js-joda` npm package: +5. In the `webApp/src/webMain/kotlin/.../main.kt` file, use the `@JsModule` annotation to import the `js-joda` npm package: ```kotlin import androidx.compose.ui.ExperimentalComposeUiApi @@ -90,7 +101,8 @@ To use the `kotlinx-datetime` library: ## Enhance the user interface -1. Open the `composeApp/src/commonMain/kotlin/App.kt` file and add the following function which returns a string containing the current date: +1. Open the `shared/src/commonMain/kotlin/App.kt` file and after the `App()` composable add the following function + which returns a string containing the current date: ```kotlin fun todaysDate(): String { @@ -138,8 +150,6 @@ To use the `kotlinx-datetime` library: ``` 4. Follow the IDE's suggestions to import the missing dependencies. - Make sure to import all the missing dependencies for the `todaysDate()` function from the updated packages, and - opt in when prompted by the IDE. ![Unresolved references](compose-unresolved-references.png) diff --git a/topics/development/multiplatform-spm-import.md b/topics/development/multiplatform-spm-import.md new file mode 100644 index 00000000..358551cb --- /dev/null +++ b/topics/development/multiplatform-spm-import.md @@ -0,0 +1,303 @@ +[//]: # (title: Swift package import in a KMP project) + + + + + This is a remote integration method. It can work for you if:
+ + * You want to separate the codebase of your final application from the common codebase. + * You've already set up a Kotlin Multiplatform project targeting iOS on your local machine. + * You use the Swift package manager for handling dependencies in your iOS project.
+ + [Choose the integration method that suits you best](multiplatform-ios-integration-overview.md) +
+ +You can set up the Kotlin/Native output for an Apple target to be consumed as a Swift package manager (SPM) dependency. + +Consider a Kotlin Multiplatform project that has an iOS target. You may want to make this iOS binary available +as a dependency to iOS developers working on native Swift projects. Using Kotlin Multiplatform tooling, you can provide +an artifact that would seamlessly integrate with their Xcode projects. + +This tutorial shows how to do this by building [XCFrameworks](multiplatform-build-native-binaries.md#build-xcframeworks) +with the Kotlin Gradle plugin. + +## Set up remote integration + +To make your framework consumable, you'll need to upload two files: + +* A ZIP archive with the XCFramework. You'll need to upload it to a convenient file storage with direct access (for example, + creating a GitHub release with the archive attached, using Amazon S3 or Maven). + Choose the option that is easiest to integrate into your workflow. +* The `Package.swift` file describing the package. You'll need to push it to a separate Git repository. + +#### Project configuration options {initial-collapse-state="collapsed" collapsible="true"} + +In this tutorial, you'll store your XCFramework as a binary in your preferred file storage, and the `Package.swift` file +in a separate Git repository. + +However, you can configure your project differently. Consider the following options for organizing Git repositories: + +* Store the `Package.swift` file and the code that should be packaged into an XCFramework in separate Git repositories. + This allows versioning the Swift manifest separately from the project the file describes. This is the recommended approach: + it allows scaling and is generally easier to maintain. +* Put the `Package.swift` file next to your Kotlin Multiplatform code. This is a more straightforward approach, but + keep in mind that, in this case, the Swift package and the code will use the same versioning. SPM uses + Git tags for versioning packages, which can conflict with tags used for your project. +* Store the `Package.swift` file within the consumer project's repository. This helps to avoid versioning and maintenance issues. + However, this approach can cause problems with multi-repository SPM setups of the consumer project and further automation: + + * In a multi-package project, only one consumer package can depend on the external module (to avoid dependency conflicts + within the project). So, all the logic that depends on your Kotlin Multiplatform module should be encapsulated in a + particular consumer package. + * If you publish the Kotlin Multiplatform project using an automated CI process, this process would need to include + publishing the updated `Package.swift` file to the consumer repository. This may lead to conflicting updates of the + consumer repository and so such a phase in CI can be difficult to maintain. + +### Configure your multiplatform project + +In the following example, the shared code of a Kotlin Multiplatform project is stored locally in the `shared` module. +If your project is structured differently, substitute "shared" in code and path examples with your module's name. + +To set up the publishing of an XCFramework: + +1. Update your `shared/build.gradle.kts` configuration file with the `XCFramework` call in the iOS targets list: + + ```kotlin + import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + + kotlin { + // Other Kotlin Multiplatform targets + // ... + // Name of the module to be imported in the consumer project + val xcframeworkName = "Shared" + val xcf = XCFramework(xcframeworkName) + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + baseName = xcframeworkName + + // Specify CFBundleIdentifier to uniquely identify the framework + binaryOption("bundleId", "org.example.${xcframeworkName}") + xcf.add(this) + isStatic = true + } + } + //... + } + ``` + +2. Run the Gradle task to create the framework: + + ```shell + ./gradlew :shared:assembleSharedXCFramework + ``` + + The resulting framework will be created as the `shared/build/XCFrameworks/release/Shared.xcframework` folder in your project directory. + + > In case you work with a Compose Multiplatform project, use the following Gradle task: + > + > ```shell + > ./gradlew :composeApp:assembleSharedXCFramework + > ``` + > + > You can then find the resulting framework in the `composeApp/build/XCFrameworks/release/Shared.xcframework` folder. + > + {style="tip"} + +### Prepare the XCFramework and the Swift package manifest + +1. Compress the `Shared.xcframework` folder in a ZIP file and calculate the checksum for the resulting archive, for example: + + `swift package compute-checksum Shared.xcframework.zip` + +2. Upload the ZIP file to the file storage of your choice. The file should be accessible + by a direct link. For example, here's how you can do it using releases in GitHub: + + + + +
  • Go to GitHub and log in to your account.
  • +
  • Navigate to the repository where you want to create a release.
  • +
  • In the Releases section on the right, click the Create a new release link.
  • +
  • Fill in the release information, add or create a new tag, specify the release title and write a description.
  • +
  • +

    Upload the ZIP file with the XCFramework through the Attach binaries by dropping them here or selecting them field at the bottom:

    + Fill in the release information +
  • +
  • Click Publish release.
  • +
  • +

    Under the Assets section of the release, right-click on the ZIP file and select Copy link address or a similar option in your browser:

    + Copy the link to the uploaded file +
  • +
    +
    +
    + +3. [Recommended] Check that the link works and that the file can be downloaded. In the terminal, run the following command: + + ```none + curl + ``` + +4. Choose any directory and locally create a `Package.swift` file with the following code: + + ```Swift + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "Shared", + platforms: [ + .iOS(.v14), + ], + products: [ + .library(name: "Shared", targets: ["Shared"]) + ], + targets: [ + .binaryTarget( + name: "Shared", + url: "", + checksum:"") + ] + ) + ``` + +5. In the `url` field, specify the link to your ZIP archive with the XCFramework. +6. [Recommended] To validate the resulting manifest, you can run the following shell command in the directory + with the `Package.swift` file: + + ```shell + swift package reset && swift package show-dependencies --format json + ``` + + The output will describe any errors found or show the successful download and parsing result if the manifest is correct. + +7. Push the `Package.swift` file to your remote repository. Make sure to create and push a Git tag with the + semantic version of the package. + +### Add the package dependency + +Now that both files are accessible, you can add the dependency on the package you created to an existing client iOS +project or create a new project. To add the package dependency: + +1. In Xcode, choose **File | Add Package Dependencies**. +2. In the search field, enter the URL of the Git repository with the `Package.swift` file inside: + + ![Specify repo with the package file](multiplatform-spm-url.png) + +3. Click the **Add package** button, then select products and corresponding targets for the package. + + > If you're making a Swift package, the dialog is different. In this case, click the **Copy package** button. + > This puts a `.package` line in your clipboard. Paste this line into the [Package.Dependency](https://developer.apple.com/documentation/packagedescription/package/dependency) + > block of your own `Package.swift` file and add the necessary product to the appropriate `Target.Dependency` block. + > + {style="tip"} + +### Check your setup + +To check that everything is set up correctly, test the import in Xcode: + +1. In your project, navigate to your UI view file, for example, `ContentView.swift`. +2. Replace the code with the following snippet: + + ```Swift + import SwiftUI + import Shared + + struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world! \(Shared.Platform_iosKt.getPlatform().name)") + } + .padding() + } + } + + #Preview { + ContentView() + } + ``` + + Here, you import the `Shared` XCFramework and then use it to obtain the platform name in the `Text` field. + +3. Ensure that the preview is updated with the new text. + +## Exporting multiple modules as an XCFramework + +To make code from several Kotlin Multiplatform modules available as an iOS binary, combine these modules in a single +umbrella module. Then, build and export the XCFramework of this umbrella module. + +For example, you have a `network` and a `database` module, which you combine in an `together` module: + +1. In the `together/build.gradle.kts` file, specify dependencies and the framework configuration: + + ```kotlin + kotlin { + val frameworkName = "together" + val xcf = XCFramework(frameworkName) + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + // Same as in the example above, + // with added export calls for dependencies + iosTarget.binaries.framework { + export(projects.network) + export(projects.database) + + baseName = frameworkName + xcf.add(this) + } + } + + // Dependencies set as "api" (as opposed to "implementation") to export underlying modules + sourceSets { + commonMain.dependencies { + api(projects.network) + api(projects.database) + } + } + } + ``` + +2. Each of the included modules should have its iOS targets configured, for example: + + ```kotlin + kotlin { + android { + //... + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + //... + } + ``` + +3. Create an empty Kotlin file inside the `together` folder, for example, `together/src/commonMain/kotlin/Together.kt`. + This is a workaround, as the Gradle script currently cannot assemble a framework if the exported module does not + contain any source code. + +4. Run the Gradle task that assembles the framework: + + ```shell + ./gradlew :together:assembleTogetherReleaseXCFramework + ``` + +5. Follow the steps from the [previous section](#prepare-the-xcframework-and-the-swift-package-manifest) to prepare + `together.xcframework`: archive it, calculate the checksum, upload the archived XCFramework to the file storage, + create and push a `Package.swift` file. + +Now, you can import the dependency into an Xcode project. After adding the `import together` directive, +you should have classes from both the `network` and `database` modules available for import in Swift code.