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 Studio | 2024.1.1+ | 需安装 KMP 插件 |
| Xcode | 15.0+ | iOS 运行 + 编译 |
| JDK | 17+ | 编译 Compose 必需 |
| Kotlin | 2.0.0+ | 支持 K2 编译器 |
2.2 创建 CMP 项目
打开 Android Studio:
File → New → New 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 参数,启用 LazyColumn 的 contentType |
| 图片优化 | 使用 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 稳定版实战。