Compose Multiplatform: Core Concepts for Cross-Platform App Development

Kotlin Multiplatform (KMP) combined with Jetpack Compose is revolutionizing cross-platform app development. By leveraging these technologies, developers can write code once and deploy it across multiple platforms such as Android, iOS, desktop, and web, all while maintaining a native look and feel. Jetpack Compose, renowned for its declarative UI approach, is particularly powerful when integrated into a KMP project. This article dives into the core concepts required to build a Compose Multiplatform app.

What is Compose Multiplatform?

Compose Multiplatform is a declarative UI framework developed by JetBrains, based on Jetpack Compose, which allows you to share UI code across multiple platforms including Android, iOS, desktop, and web. It combines the power of Kotlin Multiplatform with a modern UI toolkit to provide an efficient and flexible approach to cross-platform development.

Key Benefits of Compose Multiplatform

  • Code Sharing: Maximize code reuse across platforms.
  • Native Look and Feel: UI elements are rendered using native widgets.
  • Declarative UI: Simplified UI development with Compose.
  • Faster Development: Reduced time and effort with a single codebase.
  • Consistency: Ensures consistent branding and features across all platforms.

Core Concepts for Building Compose Multiplatform Apps

To successfully build a Compose Multiplatform app, you need to grasp several core concepts:

1. Project Setup

The first step is setting up a Kotlin Multiplatform project using Gradle. This involves configuring the build settings to target multiple platforms.


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

kotlin {
    jvm()
    androidTarget()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("androidx.appcompat:appcompat:1.6.1")
                implementation("androidx.core:core-ktx:1.12.0")
                implementation("org.jetbrains.compose:compose-jb-android-target-api:${compose.version}")
            }
        }
        val iosX64Main by getting()
        val iosArm64Main by getting()
        val iosSimulatorArm64Main by getting()
        val iosMain by getting {
            dependsOn(commonMain)
        }
        val jvmMain by getting {
            dependencies {
                implementation(compose.desktop.currentOs)
            }
        }
    }
}

2. Shared Code Structure

Organize your project into modules to separate platform-specific code from shared code. Common modules typically include:

  • commonMain: Contains code shared across all platforms, including UI logic and business logic.
  • androidMain: Contains Android-specific code.
  • iosMain: Contains iOS-specific code.
  • jvmMain: Contains desktop-specific code.

Here’s an example project structure:


MyComposeMultiplatformApp/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
    ├── androidMain/
    │   └── kotlin/
    │       └── com/
    │           └── example/
    │               └── myapp/
    │                   └── MainActivity.kt
    ├── commonMain/
    │   └── kotlin/
    │       └── com/
    │           └── example/
    │               └── myapp/
    │                   └── SharedApp.kt
    ├── iosMain/
    │   └── kotlin/
    │       └── com/
    │           └── example/
    │               └── myapp/
    │                   └── Main.kt
    └── jvmMain/
        └── kotlin/
            └── com/
                └── example/
                    └── myapp/
                        └── Main.kt

3. UI Composition with Jetpack Compose

Use Jetpack Compose to define your UI. Common UI elements can be written once and shared across platforms.


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

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

4. Platform-Specific Adaptations

While Compose handles UI rendering, some aspects like theming or device-specific features might require platform-specific implementations. Use expect/actual declarations to handle these differences.


// In commonMain
expect fun platformName(): String

// In androidMain
actual fun platformName(): String = "Android"

// In iosMain
actual fun platformName(): String = "iOS"

5. State Management

Efficiently manage your application’s state to ensure UI updates are consistent and performant. Libraries like kotlinx.coroutines and MutableState are essential.


import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.Composable

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }

    androidx.compose.material.Button(onClick = { count.value++ }) {
        Text("Increment: ${count.value}")
    }
}

6. Data Handling and Business Logic

Centralize data handling and business logic in the commonMain module to maximize code sharing. Utilize Kotlin’s coroutines for asynchronous operations and data processing.


import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.Composable

@Composable
fun fetchData() {
    val data = remember { mutableStateOf("Loading...") }

    androidx.compose.material.Button(onClick = {
        CoroutineScope(Dispatchers.Main).launch {
            data.value = getDataFromNetwork()
        }
    }) {
        Text(data.value)
    }
}

//Assume getDataFromNetwork() returns a String after a network call
suspend fun getDataFromNetwork(): String {
   delay(1000) //Simulate Network Call Delay
   return "Data from Network"
}

7. Dependency Injection

Use dependency injection frameworks like Koin or Kodein to manage dependencies in a multiplatform project, ensuring a clean and maintainable architecture.


import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koin.dsl.single
import org.koin.core.annotation.Single

// Define Koin modules
val appModule = module {
    single { MyService() }
}

// In commonMain entry point
fun initKoin() {
    startKoin {
        modules(appModule)
    }
}

Practical Example: A Simple Counter App

Let’s walk through a basic example of creating a multiplatform counter app.

1. Shared Counter Composable


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

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }

    Button(onClick = { count.value++ }) {
        Text("Increment: ${count.value}")
    }
}

2. Platform-Specific Entry Points

Android (androidMain):


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.myapp.Counter

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

iOS (iosMain):


import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController
import com.example.myapp.Counter

fun MainViewController(): UIViewController {
    return ComposeUIViewController {
        Counter()
    }
}

3. JVM (Desktop) Entry Point


import androidx.compose.desktop.Window
import androidx.compose.ui.window.application
import com.example.myapp.Counter

fun main() = application {
    Window {
        Counter()
    }
}

Conclusion

Building Compose Multiplatform apps requires understanding core concepts like project setup, shared code structure, UI composition with Jetpack Compose, platform-specific adaptations, state management, and dependency injection. By leveraging these concepts effectively, developers can create efficient, maintainable, and consistent cross-platform applications. Embracing Kotlin Multiplatform and Jetpack Compose can significantly reduce development time and effort, ensuring a unified user experience across all supported platforms. The integration offers unparalleled code reuse and a modern, declarative approach to UI development, positioning developers to deliver high-quality apps across diverse environments.