Compose Multiplatform UI: Building Shared UI Components with Jetpack Compose

Jetpack Compose has revolutionized Android UI development with its declarative approach and Kotlin-first paradigm. But its reach doesn’t stop at Android. Thanks to Kotlin Multiplatform, you can now use Jetpack Compose to build UI components that run on multiple platforms, including Android, iOS, Desktop, and Web. This blog post explores creating and utilizing Compose Multiplatform UI components in a Jetpack Compose project.

What is Compose Multiplatform?

Compose Multiplatform is a declarative UI framework for Kotlin that allows you to build applications for Android, iOS, desktop (JVM), and web, all from a single codebase. Developed by JetBrains, it leverages the power of Jetpack Compose for UI rendering across multiple platforms.

Why Use Compose Multiplatform?

  • Code Reusability: Write UI components once and use them across multiple platforms, reducing development time and effort.
  • Consistent UI/UX: Maintain a consistent look and feel across your applications, improving user experience.
  • Modern UI Development: Embrace the declarative and reactive programming style of Jetpack Compose.
  • Kotlin-First: Built on Kotlin, it’s fully interoperable with existing Kotlin libraries and frameworks.

Setting Up a Compose Multiplatform Project

Before creating Compose Multiplatform UI components, you need to set up a Multiplatform project.

Step 1: Create a New Kotlin Multiplatform Project

Use the Kotlin Multiplatform wizard in IntelliJ IDEA or Android Studio to create a new project. Select the platforms you want to target (Android, iOS, Desktop, Web).

Step 2: Project Structure

A typical Compose Multiplatform project structure includes the following source sets:

  • commonMain: Contains code shared across all platforms, including UI components.
  • androidMain: Contains Android-specific code.
  • iosMain: Contains iOS-specific code.
  • desktopMain: Contains Desktop (JVM)-specific code.
  • jsMain: Contains JavaScript (Web)-specific code.

Step 3: Add Dependencies

Ensure your build.gradle.kts file contains the necessary Compose Multiplatform dependencies:


plugins {
    kotlin("multiplatform") version "1.9.0"
    id("org.jetbrains.compose") version "1.5.1"
}

kotlin {
    androidTarget()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()
    js(IR) {
        browser()
        nodejs()
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.uiToolingPreview)
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
            }
        }
        val androidMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation("androidx.appcompat:appcompat:1.6.1")
                implementation("androidx.core:core-ktx:1.12.0")
                implementation(compose.uiTooling)
            }
        }
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting {
            dependsOn(iosMain)
        }
        val iosMain by getting {
            dependsOn(commonMain)
            dependencies {
               implementation(compose.uikit)
            }
        }
        val desktopMain by getting {
            dependsOn(commonMain)
            dependencies {
               implementation(compose.desktop.common)
               implementation(compose.desktop.currentOs)
            }
        }
        val jsMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation(compose.html.core)
            }
        }
    }
}

compose {
    kotlinCompilerPlugin.set(compose.kotlinCompilerPlugin.get())
}

Creating Compose Multiplatform UI Components

Step 1: Define UI Components in commonMain

Create UI components in the commonMain source set to make them available across all platforms.


// commonMain/kotlin/MyComponent.kt
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

@Composable
fun MyMultiplatformComponent(text: String) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("Common Component:")
        Text(text)
        Greeting(name = "Multiplatform User")
    }
}

Step 2: Implement Platform-Specific Code (if needed)

For certain UI elements or platform-specific behaviors, use expect/actual declarations to handle differences.


// commonMain/kotlin/Platform.kt
expect fun getPlatformName(): String

// androidMain/kotlin/Platform.kt
actual fun getPlatformName(): String {
    return "Android"
}

// iosMain/kotlin/Platform.kt
actual fun getPlatformName(): String {
    return "iOS"
}

// desktopMain/kotlin/Platform.kt
actual fun getPlatformName(): String {
    return "Desktop"
}

// jsMain/kotlin/Platform.kt
actual fun getPlatformName(): String {
    return "Web"
}

// commonMain/kotlin/MyComponent.kt
import androidx.compose.runtime.Composable
import androidx.compose.material.Text

@Composable
fun PlatformText() {
    Text("Running on: ${getPlatformName()}")
}

Step 3: Using UI Components in Platform-Specific Code

Now, you can use these components in your platform-specific UI code. Here’s how you would integrate them into an Android activity:


// androidMain/kotlin/MainActivity.kt
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.MaterialTheme

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                MyMultiplatformComponent(text = "Hello from Android!")
                PlatformText()
            }
        }
    }
}

For iOS:


// iosMain/kotlin/Main.kt
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController

fun MainViewController(): UIViewController {
    return ComposeUIViewController {
        MyMultiplatformComponent(text = "Hello from iOS!")
        PlatformText()
    }
}

For Desktop:


// desktopMain/kotlin/Main.kt
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title = "Compose Multiplatform Desktop") {
        DesktopContent()
    }
}

@Composable
@Preview
fun DesktopContent() {
    MyMultiplatformComponent(text = "Hello from Desktop!")
    PlatformText()
}

And finally for Web:


// jsMain/kotlin/Main.kt
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import org.jetbrains.skiko.wasm.onWasmReady

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
    onWasmReady {
        CanvasBasedWindow("Compose Multiplatform Web") {
            MyMultiplatformComponent(text = "Hello from the Web!")
            PlatformText()
        }
    }
}

Benefits and Considerations

  • Benefits:
    • Single codebase for multiple platforms
    • Reduced development costs
    • Consistent UI/UX
    • Modern declarative UI paradigm
  • Considerations:
    • Platform-specific nuances may require expect/actual declarations
    • Debugging multiplatform code can be more complex
    • Some platform-specific UI paradigms may not fully align

Conclusion

Compose Multiplatform enables developers to create reusable UI components across various platforms using a single codebase. By leveraging Jetpack Compose’s modern UI toolkit and Kotlin’s multiplatform capabilities, developers can build efficient and consistent applications for Android, iOS, Desktop, and Web. Understanding the project structure, setting up the necessary dependencies, and using expect/actual declarations for platform-specific code are crucial for successful implementation. Embrace Compose Multiplatform to streamline your development process and deliver compelling UI experiences on multiple platforms.