关于 compose-multiplatform
为 Kotlin Multiplatform 与 Compose Multiplatform 项目提供跨平台 UI 开发指导,涵盖 Android、iOS、桌面和 Web。适合需要共享 Compose 界面并处理平台差异的开发者。
主要功能
- 01区分 KMP 与 CMP 的职责
- 02规划共享与平台专属代码
- 03使用 expect/actual 实现平台差异
- 04共享字体、图片与字符串资源
- 05在 Compose 与 SwiftUI 间嵌入视图
- 06跨平台动画与手势注意事项
适用场景
- 01搭建多平台 Compose 项目结构
- 02将平台 API 接入共享界面
- 03在 iOS 应用中托管 Compose 界面
- 04排查键盘、安全区域或动画差异
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill compose-multiplatform安装后在 Claude Code 中输入/compose-multiplatform确认已加载。
详细介绍
与版本相关。 下文中的每个 API 名称、SDK 要求和浏览器支持声明均已于 2026-09-08 根据一手来源核实。核实了哪些内容以及核实时间,见
_jutsu/VERSIONS.md。如果该日期已久,请在依据版本号采取行动前重新核实。
Compose Multiplatform
Compose Multiplatform(CMP)和 Kotlin Multiplatform(KMP)的跨平台 UI 模式。 为使用
org.jetbrains.compose插件的项目加载。 基础内容:动画 API 请参阅../compose-motion/SKILL.md;本文介绍如何使用一套 Compose 代码库为 Android + iOS + Desktop + Web 编写应用的专属内容。
KMP 与 CMP——快速说明
KMP(Kotlin Multiplatform)是语言和构建基础设施:将共享 Kotlin 代码编译为 JVM、Native(iOS、macOS、Linux、Windows)和 Wasm。CMP(Compose Multiplatform)是构建在 KMP 之上的 UI 框架,由 JetBrains 开发,是 Jetpack Compose 的移植版本。你可以在 commonMain 中编写一套 Compose 代码库,在 Android、iOS、Desktop(JVM)和 Web(Wasm)上运行。平台专属代码位于 androidMain、iosMain、desktopMain、wasmJsMain 中,并通过 expect/actual 声明接入。
项目结构
composeApp/
├── src/
│ ├── commonMain/ ← shared Compose code (most of the app)
│ │ └── kotlin/
│ ├── androidMain/ ← Android-specific (uses Activity, Context)
│ ├── iosMain/ ← iOS-specific (uses UIKit/UIView interop)
│ ├── desktopMain/ ← JVM desktop (uses java.awt/swing if needed)
│ └── wasmJsMain/ ← Wasm web target
├── build.gradle.kts
iosApp/ ← Xcode project consuming the generated framework
androidApp/ ← Android Application module (often merged into composeApp)
在架构良好的 CMP 项目中,commonMain 文件夹应包含 80-95% 的代码。如果 iosMain 或 androidMain 的代码开始增长到几百行以上,你很可能正将本可留在共享层的 UI 逻辑混入平台相关逻辑。
expect/actual 模式
当确实需要为不同目标平台采用不同实现时,KMP 提供了这一应急方案。在 commonMain 中声明一次契约,并为每个目标平台各实现一次。
// commonMain
expect fun openShareSheet(text: String)
// androidMain
actual fun openShareSheet(text: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
context.startActivity(Intent.createChooser(intent, null))
}
// iosMain
actual fun openShareSheet(text: String) {
val activityVC = UIActivityViewController(
activityItems = listOf(text),
applicationActivities = null
)
UIApplication.sharedApplication.keyWindow
?.rootViewController
?.presentViewController(activityVC, true, null)
}
expect/actual 适用于顶层函数、类、类型别名和属性。actual 中的签名必须完全匹配,包括修饰符和默认值。
Composable 的 expect/actual
Composable 遵循相同规则。当某项功能需要平台专属的 Compose API(Android RuntimeShader、iOS UIKitView、Desktop SwingPanel)时,这种方式很有用。
// commonMain
@Composable
expect fun PlatformBlur(modifier: Modifier = Modifier, content: @Composable () -> Unit)
// androidMain (uses RuntimeShader on Android 13+)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier.graphicsLayer { renderEffect = blurEffect }) { content() }
}
// iosMain (uses UIVisualEffectView via UIKitView)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier) {
UIKitView(
factory = {
UIVisualEffectView(
effect = UIBlurEffect.effectWithStyle(
UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial
)
)
},
modifier = Modifier.matchParentSize()
)
content()
}
}
规则:expect Composable 应是例外,而非常规做法。大多数“平台体验”差异都可以通过在 commonMain 中调整 tokens(颜色、圆角半径、弹簧刚度)来实现,而不必采用独立代码路径。
跨平台的 LocalDensity
在 Android 上,LocalDensity.current.density 反映设备的 DPI 档位(1.0、1.5、2.0、3.0……)。在 iOS 上,密度根据 UIScreen.scale 计算(Retina 屏幕通常为 2.0 或 3.0)。在 Desktop 上,密度取决于屏幕缩放比例(默认为 1.0;Retina 级显示屏为 2.0;Windows 上可由用户配置)。在 Wasm 上,密度遵循 window.devicePixelRatio。
不要对 Dp 与像素的比例进行硬编码;应依赖 Dp 和 LocalDensity 处理转换。如果需要精确的像素值(例如用于 Canvas 绘制操作),请显式进行转换:
val density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }
避免在高频循环中读取 density;请缓存转换结果。
LocalConfiguration 与平台感知 UI
LocalConfiguration.current 仅适用于 Android,位于 androidMain 中。对于 CMP,请优先使用跨平台替代方案:
LocalWindowInfo.current.containerSize——窗口/屏幕大小,类型为IntSize,可在commonMain中使用。LocalDensity.current——密度,可在commonMain中使用。LocalLayoutDirection.current——LTR / RTL。BoxWithConstraints { ... }——在布局内部直接读取maxWidth/maxHeight。
如果需要获取真实设备特征(方向、设备类型、型号),请使用 expect/actual 封装访问,并将 PlatformInfo 等类型化对象传递到共享层。
通过 Compose Resources 实现跨平台字体
org.jetbrains.compose.resources 是共享资源插件。将字体放在 commonMain/composeResources/font/ 中,Gradle 插件会生成一个类型化的 Res 访问器。
composeApp/src/commonMain/composeResources/
├── font/
│ ├── Inter-Regular.ttf
│ └── Inter-Bold.ttf
├── drawable/
│ └── logo.xml ← Android XML vector. SVG works on every target EXCEPT Android
└── values/
├── strings.xml ← default locale
└── values-fr/
└── strings.xml ← French overrides (qualifier goes on the DIRECTORY)
在 commonMain 中使用:
import myproject.composeapp.generated.resources.Inter_Regular
import myproject.composeapp.generated.resources.Inter_Bold
import myproject.composeapp.generated.resources.Res
val InterFamily = FontFamily(
Font(Res.font.Inter_Regular, FontWeight.Normal),
Font(Res.font.Inter_Bold, FontWeight.Bold),
)
Text("Hello", fontFamily = InterFamily)
Res.drawable.logo(图片)和 Res.string.app_name(通过 stringResource(...) 获取的本地化字符串)也遵循相同的模式。原始文件是个例外:不会生成 Res.file.* 访问器;对于 composeResources/files/ 下的任何文件,都需要将路径字符串传给挂起函数 Res.readBytes(path),例如 val bytes = Res.readBytes("files/config.json")。
与 SwiftUI 互操作
CMP 会生成一个 UIViewController,你可以将它放入 SwiftUI 应用中。KMP 会生成一个顶层 Kotlin 函数(通常命名为 MainViewController() 或 ComposeUIViewController { ... }),并返回一个 UIViewController。使用 UIViewControllerRepresentable 将其包装起来。
// iosMain/kotlin/main.ios.kt
fun MainViewController(): UIViewController = ComposeUIViewController {
AppContent() // commonMain composable
}
// iOS app target
import SwiftUI
import ComposeApp // KMP-generated framework
struct ComposeContent: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View { ComposeContent().ignoresSafeArea() }
}
Kotlin 函数名会被改编为 Main_iosKt.MainViewController(),因为文件名是 main.ios.kt。如果符号名称与你预期不符,请检查生成的框架头文件。
Android 入口点
Android 上无需互操作流程。Activity 通过 setContent { ... } 直接承载 common 可组合项。
// androidApp/src/main/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppContent() // commonMain composable
}
}
}
如果需要将 Context 或 Activity 传入 commonMain,可以通过 DI 图公开它,也可以在 commonMain 中声明 expect class PlatformContext,并在 androidMain 中实现 actual class PlatformContext(val context: Context)。
在 Compose iOS 视图中嵌入 SwiftUI/UIKit(反向操作)
使用 UIKitView 创建 UIView 工厂,或使用 UIKitViewController 创建 UIViewController 工厂。
// iosMain
UIKitView(
factory = {
UISwitch().apply {
addTarget(target, action = NSSelectorFromString("onToggle:"), forControlEvents = UIControlEventValueChanged)
}
},
modifier = Modifier.size(48.dp, 32.dp)
)
对于 SwiftUI 视图:通过 Swift @objc 桥接函数公开一个 UIHostingController 将其包装起来,然后从 Kotlin 通过生成的头文件(cinterop)调用。完整模式请参见 references/cmp-interop.md。
跨平台动画
所有动画 API(animate*AsState、AnimatedVisibility、updateTransition、SharedTransitionLayout)在 CMP 1.7+ 的所有目标平台上都以相同方式工作。在 commonMain 中编写的弹簧参数调优,在 Android 和 iOS 上会产生相同的物理效果。手势(Modifier.draggable、Modifier.pointerInput)也可跨平台使用,API 表面相同。
动画入门指南位于 ../compose-motion/SKILL.md。请注意以下跨平台差异:
- iOS 首帧速度较慢(Skia 引导);200ms 的进入动画在 Android 上感觉更紧凑,在 iOS 冷启动时则会略有延迟。
- Wasm 动画在首帧可能卡顿(JIT 预热);请预热关键路径,或在可交互之前隐藏动画。
不适用的情况(注意事项)
- iOS 上的抽屉状态:原生
ModalNavigationDrawer从屏幕起始边缘滑动打开的手势会与 iOS 的返回滑动手势冲突。请使用按钮触发,或将滑动区域向内移动至少 30dp。 LayoutDirection.Rtl的问题:Android 原生支持 RTL;iOS Compose 在 1.6 版中存在错误(文本对齐、内边距反转)。1.7 及更高版本有所改进,但请使用真实的阿拉伯语/希伯来语字符串进行验证。- 软键盘处理:
imePadding()/WindowInsets.ime在 Android 上开箱即用,在 iOS 上也已实现。CMP 中没有IOSKeyboardEventListener类型——实际可调节的是ComposeUIViewControllerConfiguration.onFocusBehavior,其默认值为OnFocusBehavior.FocusableAboveKeyboard(CMP 会将整个 Compose 视图向上平移,使获得焦点的字段避开键盘)。如果想通过imePadding()/WindowInsets.ime自行控制布局,请关闭自动平移:
保持默认行为开启的同时又应用// iosMain - imports: androidx.compose.ui.window.ComposeUIViewController, // androidx.compose.ui.uikit.OnFocusBehavior fun MainViewController(): UIViewController = ComposeUIViewController( configure = { onFocusBehavior = OnFocusBehavior.DoNothing } ) { AppContent() }imePadding(),是典型的重复调整错误;如果承载该控制器的 SwiftUI 父视图也会调整键盘位置,同样会出现此问题。 Color.parseHex(...)在 Compose 中不存在。请使用Color(0xFFRRGGBB)或编写一个简单的扩展函数。- 通过 Compose 使用 iOS 系统字体:不要回退到
FontFamily.SansSerif并期待获得 SF Pro。iOS 上的 Compose 使用自身的字体回退链。可以通过 Compose Resources 捆绑 SF Pro(需遵守许可),也可以使用UIKitView放置原生UILabel来显示系统字体文本。 - Web(Wasm)上的动画:启动开销较大,首帧偶尔会卡顿;请使用浏览器开发者工具进行性能分析,并延迟加载开销较大的动画图。
java.util.UUID、java.io.File等仅适用于 JVM 的 API,在目标平台包含 iOS 或 Wasm 时,禁止在commonMain中使用。请使用kotlinx.uuid、kotlinx-io或多平台版okio。
CMP 版本说明
- 当前稳定版为 Compose Multiplatform 1.12.0(于 2026 年 8 月 25 日发布);1.10.3 是此前长期维护的版本线。对于跨平台的
SharedTransitionLayout、iOS 键盘处理以及 iOS 上的LocalLifecycleOwner,应将 1.7 视为历史最低版本,而非目标版本。 - 自 1.7 起,iOS 互操作 API 位于
androidx.compose.ui.viewinterop(UIKitView、UIKitViewController、UIKitInteropProperties);旧版androidx.compose.ui.interop.UIKitView重载中带有interactive:/accessibilityEnabled:参数的版本已标记为@Deprecated。 - 1.10 新增了
UIKitInteropProperties(placedAsOverlay = true),可将互操作视图合成在 Compose 内容之上;此外还支持固有尺寸(sizeThatFits),可将 SwiftUI 视图包装在UIHostingController中。 - 使用 K2 编译器的 Kotlin 2.x;请将最新 CMP 与最新 Kotlin 配套使用(iOS/Web 目标平台需 Kotlin 2.2.20+)。
- 部分 Material 3 组件在不同平台上的外观有所差异(例如,iOS 上的
Switch会自动呈现为 iOS 风格的比例;DatePicker则在所有目标平台上都保持 Material 风格)。 compose-multiplatform-resources插件是资源管理的标准方案;新项目不再推荐使用较旧的moko-resources。
性能注意事项
- iOS 首帧比 Android 慢(Skia 冷启动初始化约需 150–300ms)。请让启动画面保持显示,直到首次组合发出,或使用透明的根组合项进行预热。
- Wasm 包体积:目标是压缩后小于 2MB。请精简依赖、使用
kotlinx.coroutines延迟组合来延迟加载次要屏幕,并检查wasmJsBrowserDistribution中的.wasm输出。 - 桌面端:JVM 冷启动速度很快;除非需要单文件二进制文件,否则在桌面端通过 Kotlin/Native 进行 AOT 编译并不值得。
- Android:基准与 Jetpack Compose 相同——请使用 Compose 编译器稳定性指标和
Layout Inspector的重组次数进行性能分析。
反模式
| 错误做法 | 正确做法 | 原因 |
|---|---|---|
在 commonMain 中使用反射技巧或 System.getProperty("os.name") 检测平台 | 使用带类型的 Platform 对象配合 expect/actual | 反射在 Wasm/Native 上会失效;expect/actual 是由编译器强制执行的契约 |
假设在 commonMain 中可以访问 Android Context | 通过 expect class PlatformContext 或 DI 作用域注入带类型的依赖 | iOS/Desktop/Wasm 上不存在 Context;面向这些目标平台编译时,代码会失败 |
| 硬编码在 Android 上效果出色、但在 iOS 上显得突兀的 Material 颜色 | 定义 commonMain 设计系统,然后视情况通过 actual 调整 2-3 个设计令牌 | 跨平台一致性固然重要,但 iOS 用户会觉得 Material 蓝在 iPhone 上格格不入 |
在 commonMain 中使用 LaunchedEffect(Unit) { while(true) { delay(16); ... } } | 使用 rememberInfiniteTransition(),或将其限定在生命周期事件内 | 紧密循环的协程会加快 iOS 上的电量消耗;无限动画在视图移出屏幕时会暂停 |
快速参考:加载子技能
| 需求 | 加载 |
|---|---|
| 深入了解 iOS / Android 互操作 | references/cmp-interop.md |
| 各平台行为目录 | references/cmp-platform-quirks.md |
| 动画 API | ../compose-motion/SKILL.md |
| 高级图形(M3 Expressive、仅限 Android 的 AGSL) | ../compose-graphics/SKILL.md |
| 与 SwiftUI 进行 iOS 端原生互操作 | ../swiftui-motion/SKILL.md(目标为 iOS 且需要与原生 SwiftUI 融合时) |
| 移动端 UX 背景 | ../mobile-principles/SKILL.md |
| 桌面端 UX 背景 | ../desktop-principles/SKILL.md |
| 基础 | ../motion-principles/SKILL.md |
来源
- Meet-Miyani/compose-skill(全面的 KMP/CMP 资料)
- JetBrains Compose Multiplatform
- Compose Multiplatform 文档
- skydoves/Orbital(支持 KMP 的共享转场)