Compose Multiplatform 初体验:从 KMP 原理到多端跑通第一个 App
讲清 Compose Multiplatform 与 Kotlin Multiplatform、Jetpack Compose 的关系;梳理 shared / expect-actual 架构;手把手用 KMP Wizard 创建项目,在 Android、Desktop 与 iOS 上跑通共享 Composable。
为什么要关注 CMP? 如果你已经在用 Jetpack Compose 写 Android,却还要为 iOS 再维护一套 SwiftUI / UIKit,或者在 Desktop 上重写 UI——Compose Multiplatform(CMP) 提供了一条「同一套声明式 UI,编译到多个平台」的路径。它不是概念演示,JetBrains 与 Google 已在 Toolbox、KDoctor、Android Studio 部分新特性里用它落地。
这篇按 生态定位 → 架构原理 → 工程结构 → 上手实操 → 初体验结论 展开,目标是你读完后能 自己创建项目并在至少两个平台上跑起来。
一、生态定位:KMP、Compose、CMP 分别是什么
┌─────────────────────────────────────────────────────────────────┐
│ Kotlin Multiplatform (KMP) │
│ 语言级跨端:共享业务逻辑,UI 各写各的(或部分共享) │
│ expect / actual · 共享 ViewModel · 网络 / 数据库 / 算法 │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Compose Multiplatform (CMP) │
│ 在 KMP 之上共享 UI:同一套 @Composable 编译到多端 │
│ Android · iOS · Desktop(JVM) · Web(Wasm) │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Jetpack Compose (Android) │
│ Android 专用声明式 UI 框架;CMP 与其 API 高度同源 │
└─────────────────────────────────────────────────────────────────┘| 概念 | 维护方 | 共享什么 | 典型场景 |
|---|---|---|---|
| KMP | JetBrains | 逻辑层(Repository、UseCase、序列化) | 双端共享网络与领域模型,UI 仍用 Compose + SwiftUI |
| CMP | JetBrains + Google 协作 | 逻辑 + UI 层 Composable | 工具类 App、内容展示、表单、内部管理系统 |
| Jetpack Compose | 仅 Android UI | 纯 Android 或 Android 为主的产品 |
关键认知: CMP ≠「把 Android Compose 原样拷贝到 iOS」。底层渲染走各平台原生管线(Android 用 Android 运行时,iOS 用 Skiko + UIKit 宿主,Desktop 用 Skiko + AWT/Swing 窗口),但 开发者面对的 API 表面 与 Jetpack Compose 高度一致。
二、原理层:CMP 如何做到「写一次,跑多端」
2.1 编译模型:Source Set 与 Target
KMP 工程按 Source Set 组织代码:
commonMain ← 所有平台编译的共享代码(Composable、ViewModel、数据类)
│
├── androidMain ← Android 专属(Activity、权限、Android Context)
├── iosMain ← iOS 专属(UIViewController 桥接)
├── jvmMain ← Desktop JVM 窗口入口
└── wasmJsMain ← Web(实验性)入口| Source Set | 作用 |
|---|---|
commonMain | 默认共享;放 @Composable、ViewModel、纯 Kotlin 逻辑 |
*Main(平台) | 平台入口、无法抽象的系统 API |
commonTest | 共享单测 |
Gradle 插件 org.jetbrains.kotlin.multiplatform 负责把 commonMain 编译成各 Target 字节码 / 框架产物。
2.2 expect / actual:平台差异的正式出口
无法在 commonMain 直接调用的 API(文件路径、蓝牙、Keychain),用 expect 声明、actual 实现:
// commonMain
expect fun getPlatformName(): String
@Composable
expect fun PlatformIcon()
// androidMain
actual fun getPlatformName(): String = "Android"
@Composable
actual fun PlatformIcon() {
Icon(Icons.Default.Android, contentDescription = null)
}
// iosMain
actual fun getPlatformName(): String = "iOS"
@Composable
actual fun PlatformIcon() {
Icon(/* iOS 侧可用资源 */, contentDescription = null)
}规则:
expect与actual签名必须一致(含@Composable注解)。- 每个 Target 必须有对应
actual,否则编译失败。 - 能放 common 就别 expect——过度 expect 会让「跨端」退化成「五份实现」。
2.3 UI 运行时:Skiko 与原生宿主
| 平台 | UI 运行时 | 窗口 / 宿主 |
|---|---|---|
| Android | Compose Android Runtime | ComponentActivity.setContent { } |
| iOS | Compose + Skiko 绘制 | UIViewController 嵌入 Swift ComposeView |
| Desktop | Compose + Skiko | application { Window { } }(Compose Desktop) |
| Web | Compose + Wasm | Canvas / DOM 宿主(快速演进中) |
Skiko(Skia + Kotlin)是 CMP 在非 Android 平台绘制 UI 的底层。你在 Composable 里写的 Text、LazyColumn,最终由 Skiko 或 Android 运行时渲染到屏幕。
2.4 与 Jetpack Compose 的 API 关系
CMP 核心 UI 包为 org.jetbrains.compose.*,与 androidx.compose.* 高度相似:
// commonMain — CMP 项目典型 import
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ModifierMaterial3、Foundation、Runtime 在 CMP 中有对应实现;部分 Android 专属 API(如 LocalContext.current 直接拿 Android Context)仅在 androidMain 可用。
迁移心智: 会 Jetpack Compose,上手 CMP 的 UI 代码几乎零门槛;主要新增的是 工程结构、expect/actual、各平台 Run Configuration。
2.5 架构建议:UI 共享 vs 逻辑共享
方案 A · 仅 KMP 共享逻辑(传统双端) common: Repository + ViewModel android: Compose UI ios: SwiftUI UI
方案 B · CMP 全共享 UI(工具 / 内容类 App) common: Repository + ViewModel + 全部 @Composable 各平台: 仅 Application / Activity / ViewController 壳
方案 C · 混合(多数商业 App 现实选择) common: 核心流程 UI + 逻辑 平台层: 支付、推送、深度系统集成单独 actual
| 方案 | 优点 | 风险 |
|---|---|---|
| A | iOS 原生体验最佳 | 两套 UI 维护成本 |
| B | 迭代最快、UI 完全一致 | iOS 平台感弱、包体与性能需验证 |
| C | 平衡 | 需清晰划分 common / 平台边界 |
三、实现层:环境准备与创建第一个项目
3.1 环境清单
| 目标平台 | 必需 | 推荐版本(2025–2026) |
|---|---|---|
| 共用 | JDK 17+、Android Studio Ladybug+ / IntelliJ IDEA | Kotlin 2.0+ |
| Android | Android SDK、模拟器或真机 | AGP 8.x |
| Desktop | 无额外 SDK | 随 CMP 插件自带 |
| iOS | macOS + Xcode + CocoaPods | Xcode 15+;真机需 Apple 开发者账号 |
| Web | Node(部分模板) | 关注官方 Wasm 文档 |
插件:
- Kotlin Multiplatform — JetBrains 插件市场
- Compose Multiplatform — 与上者配合,提供项目模板与预览
也可直接用 Web 向导(无需先装 IDE):kmp.jetbrains.com
3.2 用 KMP Wizard 创建项目
推荐路径:File → New → Project → Kotlin Multiplatform → Compose Multiplatform Application
向导选项建议(初体验):
| 选项 | 建议 |
|---|---|
| Project name | CmpHello |
| Package | com.example.cmphello |
| Targets | ✅ Android、✅ iOS、✅ Desktop;Web 可选 |
| iOS framework | Regular framework(Studio 直接 Run 更省心) |
| Code sharing | Share UI(CMP 全 UI 模板) |
生成后的典型结构:
CmpHello/ ├── composeApp/ # 主 KMP 模块 │ ├── src/ │ │ ├── commonMain/kotlin/ │ │ │ └── App.kt # 共享 @Composable 入口 │ │ ├── androidMain/kotlin/ │ │ │ └── MainActivity.kt │ │ ├── iosMain/kotlin/ │ │ │ └── MainViewController.kt │ │ └── jvmMain/kotlin/ │ │ └── main.kt # Desktop Window 入口 │ └── build.gradle.kts ├── iosApp/ # Xcode 工程壳 │ ├── iosApp.xcodeproj │ └── ContentView.swift # 嵌入 Compose UIViewController └── build.gradle.kts
3.3 核心入口代码走读
共享 UI(commonMain) — 业务与界面写在这里:
// composeApp/src/commonMain/kotlin/App.kt
@Composable
@Preview
fun App() {
MaterialTheme {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
PlatformIcon()
Spacer(Modifier.height(8.dp))
Text("Running on ${getPlatformName()}")
Spacer(Modifier.height(16.dp))
Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(8.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
}Android 入口 — 极薄:
// androidMain/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}Desktop 入口 — Window + application:
// jvmMain/main.kt
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "CmpHello",
) {
App()
}
}iOS 入口 — Kotlin 提供 UIViewController,Swift 嵌入:
// iosMain/MainViewController.kt
fun MainViewController(): UIViewController =
ComposeUIViewController { App() }// iosApp/ContentView.swift(简化)
struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.all, edges: .bottom)
}
}三层壳各自 不超过十几行,其余全在 commonMain——这是 CMP 初体验里最直观的部分。
四、跑通三端:Run Configuration 与常见卡点
4.1 运行顺序建议
① composeApp [android] ← 最快验证,与纯 Compose 几乎相同
↓
② composeApp [desktop] ← 无需模拟器,启动 JVM 窗口
↓
③ composeApp [iosSimulator] ← 需 macOS + Xcode,首次编译较慢在 Android Studio 顶栏 Run Configuration 中选择对应 Target,点击 Run。
4.2 各平台初体验差异
| 平台 | 首次编译 | 热重载 / 预览 | 初体验印象 |
|---|---|---|---|
| Android | 1–3 min | Compose Preview(androidMain) | 与 Jetpack Compose 几乎无差 |
| Desktop | 1–2 min | 重启较快 | 窗口原生感好,适合内部工具 |
| iOS 模拟器 | 5–15 min(首次) | 无 Android 式 Preview | 能跑起来有「真的跨了」的体感 |
| Web | 视模板而定 | 变化快 | 适合轻量展示,生产需评估 |
4.3 高频踩坑
| 现象 | 原因 | 处理 |
|---|---|---|
iOS 编译失败 pod install | CocoaPods 未装或 Ruby 环境旧 | sudo gem install cocoapods;Xcode Command Line Tools |
Unresolved reference: compose | 插件或 BOM 版本不匹配 | 对齐 Kotlin、Compose Compiler、CMP 插件版本(看 兼容性表) |
| Desktop 白屏 | Skiko 原生库下载失败 | 检查网络 / Maven 镜像;./gradlew clean 重试 |
common 里用了 LocalContext | Android 专属 API | 下沉到 androidMain 或 expect/actual |
| iOS 字体 / 间距与 Android 不一致 | 平台默认 Typography 差异 | MaterialTheme 统一 typography;必要时平台 actual 微调 |
五、进阶一步:共享 ViewModel 与导航
初体验之后,通常会立刻遇到 状态放哪、页面怎么跳。
5.1 共享 ViewModel(KMP + CMP 常见组合)
// commonMain
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() { _count.update { it + 1 } }
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
// UI ...
}依赖 androidx.lifecycle:lifecycle-viewmodel-compose 的 KMP 版本,ViewModel 可放在 commonMain,Android / iOS / Desktop 共用同一套状态逻辑。
5.2 导航
官方推荐 Compose Navigation 的 KMP 移植 或 Voyager、Decompose 等跨端导航库。初体验阶段用「单屏 + 状态切换」即可;多页 App 再引入:
// 概念示例 — Voyager
class HomeScreen : Screen {
@Composable
override fun Content() {
// ...
}
}导航选型属于 实践层 后续专题;初体验先确认 单屏 Composable 在三端行为一致 更重要。
六、与 Flutter / React Native 的快速对照
| 维度 | Compose Multiplatform | Flutter | React Native |
|---|---|---|---|
| 语言 | Kotlin | Dart | JS / TS |
| UI 范式 | 声明式 Composable | 声明式 Widget | 声明式 Component |
| 渲染 | 原生管线 + Skiko | 自绘引擎 Skia | 桥接原生组件 |
| 与现有 Android 代码 | 无缝(同 Compose) | 需重写 | 需重写 |
| iOS 工程 | Kotlin 框架嵌入 Xcode | Flutter Engine | RN Bridge |
| 成熟生态 | 快速增长,弱于 Flutter | 非常成熟 | 成熟 |
| 适合谁 | Kotlin 团队、已有 Compose 资产 | 从零跨端、UI 强一致 | Web 前端转移动端 |
若团队主力是 Kotlin + 已有 Jetpack Compose,CMP 的边际成本最低;若从零选型且要强 UI 一致与庞大插件生态,Flutter 仍是默认对比项。
七、初体验结论:现在适不适合上生产?
7.1 已经适合
- 内部工具、Desktop 管理台、内容阅读类 App
- Android 为主、iOS 要求「功能对齐」而非「像素级原生」
- 已有 KMP 共享逻辑,希望 UI 也并入 common
7.2 仍需审慎
- 重动画、复杂手势、深度 iOS Human Interface 适配
- 对包体、冷启动极度敏感的消费级 iOS App
- 大量依赖 Android 专属 Compose 库(Map、Media3 等无 common 版)
7.3 推荐学习路径
Level 1 · 本篇文章 Wizard 建项 → Android + Desktop 跑通 → 改 commonMain 的 App.kt Level 2 · 共享逻辑 common ViewModel + Repository · expect/actual 封装平台 API Level 3 · 真实业务 网络(Ktor)· SQLDelight · 导航 · 资源(compose-resources) Level 4 · 上线 iOS 签名、TestFlight、性能与平台 actual 细化
八、小结
原理 KMP 共享逻辑 → CMP 在之上共享 @Composable commonMain + expect/actual + 各平台薄壳 非 Android 平台通过 Skiko 绘制 实现 KMP Wizard 选 Compose Multiplatform Application UI 写 App.kt(commonMain) 先 Run Android / Desktop,再跑 iOS 模拟器 初体验 与 Jetpack Compose 写法几乎相同 主要成本在工程配置与 iOS 工具链 适合 Kotlin 团队做跨端 UI 的第一站
Compose Multiplatform 不是「又一个跨端框架」的简单重复,而是 把 Android 开发者已熟悉的 Compose 心智模型延伸到 Desktop 与 iOS。第一次在三端看到同一个 Button 计数器同步工作时,你对 KMP 的价值会有比文档更直观的认识。