Compose Multiplatform UI: Sharing UI Elements Across Platforms

Jetpack Compose has revolutionized Android UI development with its declarative approach and Kotlin-based syntax. Now, with the introduction of Compose Multiplatform, the same concepts can be applied across various platforms, including iOS, Web, Desktop, and more. In this blog post, we’ll dive into how to use multiplatform UI elements in Jetpack Compose, allowing you to write code once and deploy it on multiple platforms.

What is Compose Multiplatform?

Compose Multiplatform is a declarative UI framework that enables you to build cross-platform applications with a single codebase. It leverages the power of Kotlin Multiplatform, allowing you to share business logic and UI code across Android, iOS, Web, and Desktop platforms. The core idea is to write UI elements using Jetpack Compose and then compile them for each target platform.

Why Use Compose Multiplatform?

  • Code Reusability: Write once, deploy everywhere. Share a significant portion of your UI code across different platforms.
  • Consistent UI: Maintain a consistent look and feel across all your applications.
  • Reduced Development Time: Faster development cycles due to code sharing and a unified UI framework.
  • Kotlin Power: Leverages the expressiveness and safety of Kotlin.

Setting Up a Compose Multiplatform Project

Before diving into UI elements, you’ll need to set up a Compose Multiplatform project. Here’s how:

Step 1: Create a New Project

You can use the Kotlin Multiplatform wizard in IntelliJ IDEA or Android Studio. Create a new Kotlin Multiplatform project and select the target platforms (Android, iOS, Desktop, Web, etc.).

Step 2: Project Structure

A typical Compose Multiplatform project structure includes:

  • commonMain: Shared code for all platforms. This is where you’ll put your Compose UI elements and shared logic.
  • androidMain: Android-specific code.
  • iosMain: iOS-specific code.
  • desktopMain: Desktop-specific code.
  • jsMain: Web-specific code.

Step 3: Dependencies

Ensure you have the necessary dependencies in your build.gradle.kts file (or build.gradle for Groovy DSL):


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

kotlin {
    jvm() // JVM target (for Desktop)
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "11"
            }
        }
    }
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    js(IR) {
        browser()
        nodejs()
    }
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.ui)
                @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
                implementation(compose.components.resources)
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("androidx.appcompat:appcompat:1.6.1")
                implementation("androidx.core:core-ktx:1.12.0")
                implementation(compose.uiToolingPreview)
                debugImplementation(compose.uiTooling)
            }
        }
        val iosMain by getting
        val desktopMain by getting {
            dependencies {
                implementation(compose.desktop.common)
                implementation(compose.desktop.currentOs)
            }
        }
        val jsMain by getting {
            dependencies {
                implementation(compose.html.core)
            }
        }
    }
}

compose {
    kotlinCompilerPlugin.set("1.5.1")
}

Make sure to sync your project after updating the build.gradle.kts file.

Creating Multiplatform UI Elements

Now, let’s create some basic UI elements in the commonMain source set. These elements will be shared across all platforms.

Example 1: Basic Text Element

Create a simple composable function to display text:


import androidx.compose.material.Text
import androidx.compose.runtime.Composable

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

You can then use this GreetingText composable in platform-specific code to display the greeting.

Example 2: Button Element

Create a button composable:


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun MyButton(text: String, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text(text)
    }
}

Example 3: Simple Layout

Let’s create a simple layout with a text field and a button:


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun MyScreen() {
    var text by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = text,
            onValueChange = { text = it },
            label = { Text("Enter text") }
        )
        MyButton(text = "Click me") {
            println("Text entered: $text")
        }
    }
}

Platform-Specific Implementations

To use the common UI elements, you need to integrate them into platform-specific applications.

Android

In androidMain, create an Activity and use the setContent function to render your Compose UI:


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

iOS

In iosMain, you’ll need to create an iOS application using SwiftUI interop. Here’s a basic example:


import SwiftUI
import Compose

@main
struct iOSApp: App {
    var body: some Scene {
        WindowGroup {
            ComposeUIViewControllerRepresentable()
                .ignoresSafeArea(.all)
        }
    }
}

struct ComposeUIViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

Create a MainViewController in Kotlin/Compose:


import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController

fun MainViewController(): UIViewController = ComposeUIViewController {
    MyScreen()
}

Desktop

In desktopMain, use application to create a desktop window with Compose:


import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.MaterialTheme
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 Desktop") {
        MaterialTheme {
            MyScreen()
        }
    }
}

@Preview
@Composable
fun AppPreview() {
    MaterialTheme {
        MyScreen()
    }
}

Web

In jsMain, use the renderComposable function to render Compose UI in the browser:


import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
    CanvasBasedWindow("Compose for Web") {
        MyScreen()
    }
}

Advanced Multiplatform UI

For more complex UI elements, consider these strategies:

  • Custom Renderers: For platform-specific rendering, use expect/actual declarations to define interfaces in commonMain and implement them in platform-specific modules.
  • Adaptive UI: Use screen size and platform detection to adjust the layout and UI elements dynamically.
  • State Management: Use a state management solution like Redux or MVI to manage the state of your application consistently across platforms.

Best Practices for Compose Multiplatform UI Elements

  • Keep UI Logic in Common: Move as much UI logic as possible into the commonMain module to maximize code reuse.
  • Use Abstraction: Use interfaces and abstract classes to define platform-agnostic APIs.
  • Handle Platform Differences: Use expect/actual declarations to handle platform-specific implementations.
  • Test Thoroughly: Test your UI elements on each target platform to ensure they work as expected.

Conclusion

Compose Multiplatform offers a powerful way to build cross-platform applications with a shared codebase. By leveraging Jetpack Compose, you can create consistent and reusable UI elements for Android, iOS, Web, Desktop, and more. While setting up a Multiplatform project might seem complex initially, the benefits of code reusability and reduced development time make it a compelling choice for modern application development. Start experimenting with Compose Multiplatform and see how it can transform your development workflow.