Compose Multiplatform 实战:iOS/Android/Desktop 三端共享 UI(2026)

4 阅读4分钟

Compose Multiplatform 实战:iOS/Android/Desktop 三端共享 UI(2026)

Compose Multiplatform (CMP) 让你可以共享 UI 代码到 iOS、Android、Desktop 和 Web。本文深度解析 CMP 的架构、实战搭建、各平台适配,以及与 KMP(逻辑共享)的组合使用方式。


一、什么是 Compose Multiplatform?

Compose Multiplatform = Jetpack Compose (Android) + 多平台渲染后端
                                      ↓
                 共享 UI 代码(Kotlin + Compose)
                                      ↓
                ┌──────────┬──────────┬──────────┐
              Android    iOS        Desktop    Web(Wasm)
              原生渲染   Skia通过    Skia       Skia → Wasm
                        Metal

与 KMP 的核心区别:

对比维度Kotlin Multiplatform (KMP)Compose Multiplatform (CMP)
共享内容业务逻辑(网络、数据、ViewModel)UI + 业务逻辑(全共享)
各平台 UI原生 UI(UIKit / Android View)Compose UI(自绘)
成熟度✅ 稳定(1.0+)⚠️ iOS 处于 Beta(2026 年初)
适用场景渐进式迁移,保留原生 UI从零开始的全跨平台项目

二、环境搭建(2026 最新)

2.1 软件要求

工具版本要求说明
Android Studio2024.1.1+需安装 KMP 插件
Xcode15.0+iOS 运行 + 编译
JDK17+编译 Compose 必需
Kotlin2.0.0+支持 K2 编译器

2.2 创建 CMP 项目

打开 Android Studio:

File → NewNew Project
选择 "Kotlin Multiplatform" 模板
填写:
  - Project name: "CMP Demo"
  - Package name: "com.example.cmpdemo"
  - Target platforms: ✅ Android ✅ iOS ✅ Desktop ✅ Web(Wasm)

生成的项目结构:

CMPDemo/
├── composeApp/              # ✅ Compose Multiplatform 模块(核心)
│   └── src/
│       ├── androidMain/    # Android 特定(Activity 入口)
│       ├── iosMain/        # iOS 特定(UIViewController 入口)
│       ├── desktopMain/   # Desktop 特定(application 入口)
│       ├── wasmJsMain/   # Web Wasm 特定(canvas 渲染)
│       └── commonMain/    # ✅ 共享 UI + 逻辑代码
│
├── iosApp/                # iOS 原生宿主项目
│   └── iosApp.xcodeproj
│
└── build.gradle.kts       # 根构建文件

三、composeApp 模块详解

3.1 根 build.gradle.kts

// composeApp/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.compose")          // ⭐ Compose 插件
    id("com.android.library")
    id("org.jetbrains.kotlin.multiplatform")
}

kotlin {
    androidTarget()
    ios()
    iosSimulatorArm64()  // M 系列芯片模拟器支持

    jvm("desktop")    // Desktop 目标

    @OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        browser {
            commonWebpackConfig {
                outputFileName = "composeApp.js"
            }
        }
    }

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

                // 导航
                implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0")

                // 图片加载
                implementation("io.coil-kt:coil-compose:2.6.0")
            }
        }

        val androidMain by getting {
            dependencies {
                implementation("androidx.activity:activity-compose:1.9.0")
            }
        }

        val iosMain by creating {
            dependsOn(commonMain)
        }

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

        val wasmJsMain by creating {
            dependsOn(commonMain)
        }
    }
}

四、共享 UI 代码(commonMain)

4.1 入口函数(各平台共用)

// composeApp/src/commonMain/kotlin/App.kt
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*

@Composable
fun App() {
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) {
            darkColorScheme()
        } else {
            lightColorScheme()
        }
    ) {
        var count by remember { mutableStateOf(0) }
        var showDialog by remember { mutableStateOf(false) }

        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("CMP Demo") },
                    actions = {
                        IconButton(onClick = { showDialog = true }) {
                            Icon(Icons.Default.Info, contentDescription = "About")
                        }
                    }
                )
            }
        ) { padding ->
            Column(
                modifier = Modifier.fillMaxSize().padding(padding),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(
                    text = "You clicked $count times",
                    style = MaterialTheme.typography.headlineMedium
                )
                Spacer(Modifier.height(16.dp))
                Button(onClick = { count++ }) {
                    Text("Click me")
                }
                Spacer(Modifier.height(8.dp))
                Button(
                    onClick = { count = 0 },
                    colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
                ) {
                    Text("Reset")
                }
            }

            if (showDialog) {
                AlertDialog(
                    onDismissRequest = { showDialog = false },
                    title = { Text("About") },
                    text = { Text("Compose Multiplatform Demo\nRunning on ${getPlatformName()}") },
                    confirmButton = {
                        TextButton(onClick = { showDialog = false }) {
                            Text("OK")
                        }
                    }
                )
            }
        }
    }
}

expect fun isSystemInDarkTheme(): Boolean
expect fun getPlatformName(): String

4.2 各平台特定实现

// composeApp/src/androidMain/kotlin/Platform.android.kt
actual fun isSystemInDarkTheme(): Boolean {
    val configuration = LocalContext.current.resources.configuration
    return configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}

actual fun getPlatformName(): String {
    return "Android ${Build.VERSION.SDK_INT}"
}
// composeApp/src/iosMain/kotlin/Platform.ios.kt
import platform.UIKit.UIDevice

actual fun isSystemInDarkTheme(): Boolean {
    // iOS 需要 Swift 侧传递,这里简化
    return false
}

actual fun getPlatformName(): String {
    return "iOS ${UIDevice.currentDevice.systemVersion}"
}
// composeApp/src/desktopMain/kotlin/Platform.desktop.kt
import java.awt.Toolkit

actual fun isSystemInDarkTheme(): Boolean {
    // 检测系统深色模式(简化实现)
    return false
}

actual fun getPlatformName(): String {
    val os = System.getProperty("os.name")
    return "Desktop: $os"
}

五、各平台入口配置

5.1 Android 入口

// composeApp/src/androidMain/kotlin/MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

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

5.2 iOS 入口(SwiftUI)

// iosApp/iosApp/ContentView.swift
import SwiftUI
import shared  // 来自 shared 框架(KMP 模块)

struct ContentView: View {
    var body: some View {
        ComposeView { App_iosKt() }
    }
}

struct ComposeView<Content: View>: UIViewControllerRepresentable {
    let content: () -> Content

    func makeUIViewController(context: Context) -> UIViewController {
        return MainViewControllerKt()
    }

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

5.3 Desktop 入口

// composeApp/src/desktopMain/kotlin/main.kt
import androidx.compose.ui.window.application
import androidx.compose.ui.window.Window

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title: "CMP Demo") {
        App()
    }
}

5.4 Web (Wasm) 入口

// composeApp/src/wasmJsMain/kotlin/main.kt
import androidx.compose.ui.window.ComposeViewport

fun main() {
    ComposeViewport(rootElementId = "composeApp") {
        App()
    }
}
<!-- composeApp/src/wasmJsMain/resources/index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CMP Demo</title>
    <style>html, body { margin: 0; padding: 0; }</style>
</head>
<body>
    <div id="composeApp"></div>
    <script src="composeApp.js"></script>
</body>
</html>

六、实战:网络请求 + 图片加载

6.1 添加依赖

// composeApp/build.gradle.kts → sourceSets.commonMain
dependencies {
    // Ktor 客户端
    implementation("io.ktor:ktor-client-core:3.0.0")
    implementation("io.ktor:ktor-client-content-negotiation:3.0.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")

    // Coil 图片加载
    implementation("io.coil-kt:coil-compose:2.6.0")

    // ViewModel
    implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
}

6.2 ViewModel(共享逻辑 + UI 状态)

// composeApp/src/commonMain/kotlin/UserViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    init {
        loadUsers()
    }

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val users = UserRepository().getUsers()
                _uiState.update { it.copy(users = users, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }
}

data class UserUiState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

6.3 UI 层(Compose)

// composeApp/src/commonMain/kotlin/UserScreen.kt
import androidx.compose.foundation.lazy.items
import io.coil3.compose.AsyncImage

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Users") })
        }
    ) { padding ->
        if (uiState.isLoading) {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        } else if (uiState.error != null) {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text("Error: ${uiState.error}")
                    Spacer(Modifier.height(8.dp))
                    Button(onClick = { viewModel.loadUsers() }) {
                        Text("Retry")
                    }
                }
            }
        } else {
            LazyColumn(
                modifier = Modifier.fillMaxSize().padding(padding),
                contentPadding = PaddingValues(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(uiState.users) { user ->
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        shape = RoundedCornerShape(8.dp)
                    ) {
                        Row(
                            modifier = Modifier.padding(12.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            AsyncImage(
                                model = user.avatarUrl,
                                contentDescription = user.name,
                                modifier = Modifier.size(48.dp).clip(CircleShape)
                            )
                            Spacer(Modifier.width(12.dp))
                            Column {
                                Text(user.name, style = MaterialTheme.typography.titleMedium)
                                Text(user.email, style = MaterialTheme.typography.bodyMedium)
                            }
                        }
                    }
                }
            }
        }
    }
}

七、iOS 集成注意事项(Beta 阶段)

7.1 在 Xcode 中配置框架

1. 在 Android Studio 中运行:
   ./gradlew :composeApp:assembleAppleaseXCFramework

2. 在 Xcode 中:
   Target → General → Frameworks, Libraries, and Embedded Content
   → 点击 "+" → Add Other → Add Files
   → 选择:composeApp/build/bin/iosArm64/releaseFramework/App.framework

3. Embed & Sign → 选择 "Embed & Sign"

7.2 Swift 中调用 Compose UI

// iosApp/iosApp/ContentView.swift
import SwiftUI
import UIKit
import shared

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return MainViewControllerKt()
    }

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

@main
struct iosApp: App {
    var body: some Scene {
        WindowGroup {
            ComposeView()
        }
    }
}

八、性能优化建议

优化点做法
避免重复重组使用 remember + derivedStateOf
列表性能使用 key 参数,启用 LazyColumncontentType
图片优化使用 Coil,配置 .crossfade(true) 和合适尺寸
启动优化Desktop/Web 使用 SplashScreen,延迟加载非关键 UI
包体积Web(Wasm) 使用 --optimize 编译,开启 Dead Code Elimination

九、与 KMP 的组合策略

策略一:纯 CMP(推荐用于新项目)
  shared/ (KMP 逻辑共享)
       +
  composeApp/ (CMP UI 共享)
  = 一套代码覆盖 Android + iOS + Desktop + Web

策略二:渐进式(已有原生项目)
  1. 用 KMP 共享业务逻辑(网络、数据层)
  2. 新页面用 CMP 写(逐步替换原生页面)
  3. 旧页面保留原生 UI(混合架构)

十、学习资源

资源链接
官方文档www.jetbrains.com/lp/compose-…
CMP 示例项目github.com/JetBrains/c…
Ktor 文档ktor.io/docs/
Coil 图片库coil-kt.github.io/coil/

总结

Compose Multiplatform 的核心价值是一套 UI 代码覆盖全平台

2026 年状态:
✅ Android:稳定
✅ Desktop:稳定
✅ Web(Wasm):稳定
⚠️ iOS:Beta(生产环境需评估)

适用场景:

  • ✅ 从零开始的新跨平台项目
  • ✅ 已有 Android Compose 代码,想扩展到 iOS
  • ⚠️ 对性能要求极高的游戏 / 图形应用(需评估)
  • ❌ 已有大型原生项目且不想替换 UI 层(用 KMP 更合适)

如果本文对你有帮助,欢迎点赞 + 收藏。后续会更新 CMP iOS 稳定版实战。