Compose Multiplatform: Development Tools and Techniques

Jetpack Compose is not just for building native Android UIs; it’s a modern, declarative UI toolkit that can be used to create cross-platform applications. Compose Multiplatform enables developers to write UIs once and deploy them across multiple platforms, including Android, iOS, desktop (JVM), and web. In this comprehensive guide, we’ll explore the tools and techniques for building Compose Multiplatform apps, complete with extensive code examples.

Introduction to Compose Multiplatform

Compose Multiplatform is based on Jetpack Compose, leveraging Kotlin’s multiplatform capabilities to share code between different target platforms. This approach minimizes platform-specific code and promotes a unified codebase.

Why Use Compose Multiplatform?

  • Code Reusability: Write UI logic once and deploy it on multiple platforms.
  • Unified Technology Stack: Uses Kotlin and Jetpack Compose for all platforms, simplifying development.
  • Faster Development: Reduces development time by sharing code between platforms.
  • Consistent UI/UX: Ensures a consistent look and feel across different platforms.

Setting Up a Compose Multiplatform Project

Let’s begin by setting up a new Compose Multiplatform project. The recommended approach is to use the Kotlin Multiplatform wizard in IntelliJ IDEA or Android Studio.

Step 1: Create a New Project

  • Open IntelliJ IDEA or Android Studio.
  • Select New Project.
  • Choose Kotlin Multiplatform App.
  • Configure the project name, location, and target platforms (Android, iOS, Desktop, Web).

Step 2: Project Structure

A typical Compose Multiplatform project structure includes several modules:

  • commonMain: Contains the shared code, including UI logic written in Compose.
  • androidMain: Platform-specific code for Android.
  • iosMain: Platform-specific code for iOS.
  • desktopMain: Platform-specific code for desktop (JVM).
  • jsMain: Platform-specific code for web (JavaScript).

Project/
├── androidApp/
│   └── src/androidMain/
├── iosApp/
│   └── src/iosMain/
├── desktopApp/
│   └── src/jvmMain/
├── jsApp/
│   └── src/jsMain/
└── shared/
    └── src/commonMain/

Essential Tools for Compose Multiplatform Development

1. Kotlin Multiplatform (KMP) Plugin

The Kotlin Multiplatform plugin for Gradle is essential for managing the build process across multiple platforms. It allows you to configure target platforms and share code between them.

2. Jetpack Compose

Jetpack Compose is the UI toolkit at the heart of Compose Multiplatform. Its declarative syntax and composable functions make building UIs efficient and maintainable.

3. Android Studio and IntelliJ IDEA

These IDEs provide excellent support for Kotlin and Jetpack Compose, including code completion, debugging, and project management tools.

4. Gradle

Gradle is used for building and managing the dependencies of your project. It integrates seamlessly with the Kotlin Multiplatform plugin.

5. Compose Compiler

The Compose compiler transforms Compose code into an efficient UI rendering pipeline, optimizing performance.

Building a Simple Compose Multiplatform App

Let’s create a simple “Hello, World!” app that targets Android, iOS, and desktop.

Step 1: Write Shared UI Code

In the commonMain module, define the shared UI using Compose:


// shared/src/commonMain/kotlin/App.kt
import androidx.compose.material.MaterialTheme
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
import androidx.compose.material.Text

@Composable
fun App() {
    MaterialTheme {
        Text("Hello, World!")
    }
}

@Preview
@Composable
fun AppPreview() {
    App()
}

Step 2: Platform-Specific Entry Points

Android (androidApp)

In androidApp/src/androidMain/kotlin/MainActivity.kt:


package com.example.myapplication

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

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

In iosApp/src/iosMain/kotlin/Main.kt:


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

fun MainViewController(): UIViewController {
    return ComposeUIViewController { App() }
}
Desktop (desktopApp)

In desktopApp/src/jvmMain/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 App") {
        App()
    }
}

@Preview
@Composable
fun AppDesktopPreview() {
    App()
}

Step 3: Run the Applications

Run each application to see the “Hello, World!” message on Android, iOS, and desktop.

Managing Dependencies in Compose Multiplatform

Managing dependencies in a multiplatform project involves declaring them in the build.gradle.kts file of the shared module. Use the sourceSets block to specify common and platform-specific dependencies.


kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("androidx.appcompat:appcompat:1.6.1")
                implementation("androidx.core:core-ktx:1.10.1")
                implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
                implementation("androidx.activity:activity-compose:1.7.2")
                implementation(compose.uiToolingPreview)
                implementation(compose.ui)
            }
        }
        val iosMain by getting {
            dependencies {
                implementation(compose.ui)
            }
        }
        val desktopMain by getting {
            dependencies {
                implementation(compose.desktop.ui)
            }
        }
    }
}

Implementing Platform-Specific Functionality

While the goal is to share as much code as possible, certain functionalities may require platform-specific implementations. This can be achieved using expect and actual declarations.

Example: Platform-Specific Greetings

Define an expect function in the commonMain module:


// shared/src/commonMain/kotlin/Platform.kt
expect fun platformName(): String

Provide actual implementations in the platform-specific modules:

Android (androidMain)

// androidApp/src/androidMain/kotlin/Platform.kt
actual fun platformName(): String {
    return "Android"
}
iOS (iosMain)

// iosApp/src/iosMain/kotlin/Platform.kt
actual fun platformName(): String {
    return "iOS"
}
Desktop (desktopMain)

// desktopApp/src/jvmMain/kotlin/Platform.kt
actual fun platformName(): String {
    return "Desktop"
}

Use the platform-specific implementation in the shared code:


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

@Composable
fun App() {
    MaterialTheme {
        Text("Hello from ${platformName()}!")
    }
}

Advanced UI Components

Compose Multiplatform supports a wide range of UI components. Here are a few examples:

Buttons


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

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

TextFields


import androidx.compose.material.TextField
import androidx.compose.runtime.*

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

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Enter text") }
    )
}

Lists


import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun MyList(items: List) {
    LazyColumn {
        items(items.size) { index ->
            Text(items[index])
        }
    }
}

State Management

State management is crucial for building robust Compose Multiplatform applications. The same principles and tools used in Jetpack Compose apply here, including:

  • remember and mutableStateOf: For managing local state.
  • ViewModel: For managing UI-related data in a lifecycle-conscious manner (particularly useful in Android).
  • Dependency Injection (e.g., Koin): For managing dependencies across the application.

Networking and Data Handling

For networking and data handling, Kotlinx.serialization and Ktor are commonly used libraries that are compatible with Kotlin Multiplatform.

Kotlinx.serialization

Use Kotlinx.serialization to serialize and deserialize data in a platform-independent way.


import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class User(val id: Int, val name: String, val email: String)

fun main() {
    val user = User(1, "John Doe", "john.doe@example.com")
    val jsonString = Json.encodeToString(User.serializer(), user)
    println(jsonString)

    val deserializedUser = Json.decodeFromString(User.serializer(), jsonString)
    println(deserializedUser)
}

Ktor

Use Ktor to make HTTP requests from your Compose Multiplatform app.


import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import kotlinx.coroutines.*

suspend fun fetchData(): String {
    val client = HttpClient(CIO)
    val url = "https://jsonplaceholder.typicode.com/todos/1"
    try {
        val response: String = client.get(url)
        return response
    } catch (e: Exception) {
        return "Error: ${e.message}"
    } finally {
        client.close()
    }
}

fun main() = runBlocking {
    val data = fetchData()
    println(data)
}

UI Testing

UI testing is an important part of developing high-quality Compose Multiplatform applications. While platform-specific testing tools are necessary, you can write common UI tests for shared components.

For Android, you can use Espresso; for iOS, XCTest; and for desktop, JUnit. Integrate these testing frameworks into your respective platform modules to ensure comprehensive test coverage.

Best Practices for Compose Multiplatform Development

  • Maximize Code Sharing: Aim to write as much code as possible in the commonMain module.
  • Use expect/actual Wisely: Implement platform-specific functionality only when necessary.
  • Properly Structure Dependencies: Keep dependencies organized in the build.gradle.kts file.
  • Consistent UI/UX: Design your UI to be as consistent as possible across platforms.
  • Comprehensive Testing: Write unit and UI tests for all shared and platform-specific code.

Conclusion

Compose Multiplatform is a powerful tool for building cross-platform applications with a unified technology stack. By leveraging Kotlin’s multiplatform capabilities and Jetpack Compose’s declarative UI, developers can create efficient and maintainable applications that target Android, iOS, desktop, and web. Setting up the project correctly, managing dependencies, implementing platform-specific functionalities, and following best practices are key to successful Compose Multiplatform development. With the right tools and techniques, you can build compelling cross-platform experiences that delight users on any device.