Compose Multiplatform Setup: Cross-Platform UI in Jetpack Compose

Compose Multiplatform (CMP) allows you to share your declarative UI code across multiple platforms, including Android, iOS, desktop, and web. In this comprehensive guide, we will walk you through setting up a Compose Multiplatform project using Jetpack Compose. This involves creating a Kotlin Multiplatform project, configuring dependencies, and structuring your codebase for efficient cross-platform UI development.

What is Compose Multiplatform?

Compose Multiplatform (CMP) is a declarative UI framework based on Kotlin and Jetpack Compose, allowing you to write UI code once and deploy it on multiple platforms like Android, iOS, desktop (JVM), and web. CMP promotes code reuse and simplifies UI development across diverse ecosystems.

Why Use Compose Multiplatform?

  • Code Reusability: Write UI code once and reuse it across different platforms.
  • Consistent UI: Maintain a consistent look and feel across all supported platforms.
  • Modern Declarative UI: Leverage Jetpack Compose’s declarative approach for efficient UI development.
  • Kotlin Advantage: Benefit from Kotlin’s conciseness, safety, and interoperability with platform-specific code.

Prerequisites

Before starting, make sure you have the following:

  • IntelliJ IDEA or Android Studio: Latest version installed.
  • Kotlin Plugin: Kotlin plugin installed and enabled in your IDE.
  • JDK: Java Development Kit (JDK) 11 or higher.
  • Android SDK: Required if you plan to target Android.
  • Xcode: Required for targeting iOS.

Step-by-Step Guide to Setting Up a Compose Multiplatform Project

Step 1: Create a New Kotlin Multiplatform Project

  1. Open IntelliJ IDEA or Android Studio.
  2. Click on New Project.
  3. Choose Kotlin in the left panel and select Multiplatform App.
  4. Click Next.
  5. Provide the project name, location, and desired settings.
  6. Target Platforms: Select Android, iOS, and Desktop (JVM). You can also add other targets as needed.
  7. Click Create.

After creating the project, Gradle will sync and generate the initial project structure.

Step 2: Project Structure

A typical Compose Multiplatform project structure includes:

  • commonMain: Contains the common code shared between all platforms. UI components, business logic, and data models go here.
  • androidMain: Contains Android-specific code.
  • iosMain: Contains iOS-specific code.
  • desktopMain: Contains JVM-specific code for desktop applications.
  • jsMain: Contains JavaScript-specific code for web applications.

MyCMPProject/
├── .gradle/
├── .idea/
├── androidApp/
│   ├── src/
│   │   └── androidMain/
│   │       ├── AndroidManifest.xml
│   │       └── kotlin/
├── iosApp/
│   ├── iosApp/
│   ├── ...
├── desktopApp/
│   ├── src/
│   │   └── jvmMain/
│   │       └── kotlin/
├── common/
│   ├── src/
│   │   └── commonMain/
│   │       └── kotlin/
├── build.gradle.kts
└── settings.gradle.kts

Step 3: Configure Dependencies

Update your build.gradle.kts file with the necessary Compose Multiplatform dependencies.

  1. Open build.gradle.kts in the root of your project.
  2. Add the Compose Multiplatform plugin and dependencies.

plugins {
    kotlin("multiplatform") version "1.9.22" // Replace with the latest version
    id("org.jetbrains.compose") version "1.6.0"  // Replace with the latest version
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "11"
            }
        }
    }
    
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    jvm("desktop")

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.components.resources)
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.ui)

            }
        }
        val androidMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation("androidx.appcompat:appcompat:1.6.1")
                implementation("androidx.core:core-ktx:1.12.0")
                implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
                implementation("androidx.activity:activity-compose:1.8.2")
                implementation(compose.uiTooling)
                implementation(compose.uiToolingPreview)
            }
        }
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting {
            dependsOn(iosX64Main)
        }
        val iosMain by getting {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
            dependencies {
                implementation(compose.uikit.uikit)
                implementation(compose.uikit.uikitCompose)
            }
        }

        val desktopMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation(compose.desktop.desktop)
            }
        }
    }
}


android {
    namespace = "org.example.MyCMPProject.androidApp"
    compileSdk = 34
    defaultConfig {
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"  // Replace with the latest version
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

Remember to replace 1.9.22 and 1.6.0 with the latest versions of the Kotlin and Compose plugins, respectively. Ensure that Compose compiler extension version is compatible with the compose version that you’re using. You may also need to add platform-specific configurations based on your project’s requirements.

Step 4: Create a Simple UI

Let’s create a simple UI that displays “Hello, Compose Multiplatform!” in the commonMain source set.

  1. Navigate to common/src/commonMain/kotlin.
  2. Create a new Kotlin file named App.kt.

import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.*
import androidx.compose.ui.platform.testTag

@Composable
fun App() {
    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxSize().padding(16.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Hello, Compose Multiplatform!", modifier = Modifier.testTag("greeting"))
        }
    }
}

This code defines a simple Compose UI with a Text composable.

Step 5: Integrate with Platform-Specific Code

Now, let’s integrate this common UI with the platform-specific code for Android, iOS, and desktop.

Android Integration
  1. Navigate to androidApp/src/androidMain/kotlin.
  2. Open or create MainActivity.kt.

package org.example.MyCMPProject.androidApp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import org.example.App

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App()
        }
    }
}
iOS Integration

For iOS, you need to integrate the Compose UI within your Swift code.

  1. Open the iosApp project in Xcode.
  2. Add a new Swift file named ComposeViewController.swift.

import UIKit
import SwiftUI
import Compose

class ComposeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let rootView = ComposeView()
        rootView.setContent {
            AppKt.App()
        }

        let hostingController = UIHostingController(rootView: rootView)
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)

        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
}
  1. In your MainViewController.swift, present the ComposeViewController.

  2. Update your MainViewController.swift


import UIKit

class MainViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton(type: .system)
        button.setTitle("Compose Screen", for: .normal)
        button.addTarget(self, action: #selector(openComposeView), for: .touchUpInside)

        view.addSubview(button)

        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func openComposeView() {
        let composeViewController = ComposeViewController()
        self.present(composeViewController, animated: true, completion: nil)
    }
}
Desktop Integration
  1. Navigate to desktopApp/src/jvmMain/kotlin.
  2. Open or create Main.kt.

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import org.example.App

fun main() = application {
    Window(title = "Compose Multiplatform Desktop") {
        App()
    }
}

Step 6: Run the Applications

  1. Android: Run the androidApp on an Android emulator or device.
  2. iOS: Run the iosApp on an iOS simulator or device via Xcode.
  3. Desktop: Run the desktopApp directly from IntelliJ IDEA.

You should see the “Hello, Compose Multiplatform!” text on all three platforms, confirming that your setup is correct.

Tips and Best Practices

  • Directory Structure: Organize your code efficiently to separate common and platform-specific logic.
  • UI Abstraction: Abstract UI components to maximize code sharing while allowing platform-specific customizations.
  • Dependency Management: Keep dependencies updated to leverage the latest features and bug fixes.
  • Platform-Specific Code: Use expect/actual to handle platform-specific implementations while maintaining a common API.
  • Testing: Implement thorough testing for each platform to ensure code quality and consistency.

Conclusion

Setting up a Compose Multiplatform project in Jetpack Compose allows you to efficiently share UI code across Android, iOS, and desktop platforms. By following this guide, you can create a structured and maintainable project, enabling you to deliver consistent and engaging user experiences across diverse ecosystems. Embrace Compose Multiplatform to unlock the full potential of cross-platform UI development.