compose-multiplatform
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose Multiplatform
Compose Multiplatform
Compose Multiplatform (CMP) and Kotlin Multiplatform (KMP) patterns for cross-platform UI. Loaded for projects withplugin. Foundation:org.jetbrains.composefor animation API; this file covers what's specific to writing one Compose codebase for Android + iOS + Desktop + Web.../compose-motion/SKILL.md
适用于跨平台UI的Compose Multiplatform (CMP) 和Kotlin Multiplatform (KMP) 模式。 适用于使用插件的项目。 基础内容:动画API请参考org.jetbrains.compose;本文档涵盖为Android + iOS + Desktop + Web编写单一Compose代码库的特定内容。../compose-motion/SKILL.md
KMP vs CMP - quick clarification
KMP vs CMP - 快速说明
KMP (Kotlin Multiplatform) is the language and build infrastructure: shared Kotlin code compiled to JVM, Native (iOS, macOS, Linux, Windows), and Wasm. CMP (Compose Multiplatform) is the UI framework on top of KMP, built by JetBrains as a port of Jetpack Compose. You write a single Compose codebase in that runs on Android, iOS, Desktop (JVM), and Web (Wasm). Platform-specific code lives in , , , and is wired in via / declarations.
commonMainandroidMainiosMaindesktopMainwasmJsMainexpectactualKMP(Kotlin Multiplatform)是语言与构建基础设施:共享Kotlin代码可编译为JVM、Native(iOS、macOS、Linux、Windows)和Wasm。CMP(Compose Multiplatform)是构建在KMP之上的UI框架,由JetBrains作为Jetpack Compose的移植版本开发。您在中编写单一Compose代码库,即可在Android、iOS、Desktop(JVM)和Web(Wasm)上运行。平台特定代码存放在、、、中,并通过/声明进行关联。
commonMainandroidMainiosMaindesktopMainwasmJsMainexpectactualProject structure
项目结构
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)The folder should hold 80-95% of your code in a well-architected CMP project. If or start growing past a few hundred lines, you're probably leaking platform concerns into UI logic that could stay shared.
commonMainiosMainandroidMaincomposeApp/
├── src/
│ ├── commonMain/ ← 共享Compose代码(应用的核心部分)
│ │ └── kotlin/
│ ├── androidMain/ ← Android特定代码(使用Activity、Context)
│ ├── iosMain/ ← iOS特定代码(使用UIKit/UIView互操作)
│ ├── desktopMain/ ← JVM桌面代码(必要时使用java.awt/swing)
│ └── wasmJsMain/ ← Wasm Web目标代码
├── build.gradle.kts
iosApp/ ← 消费生成框架的Xcode项目
androidApp/ ← Android应用模块(通常合并到composeApp中)在架构良好的CMP项目中,文件夹应包含80-95%的代码。如果或的代码量超过几百行,您可能是将平台相关的逻辑泄漏到了本可以保持共享的UI逻辑中。
commonMainiosMainandroidMainexpect
/actual
pattern
expectactualexpect
/actual
模式
expectactualThe KMP escape hatch when you genuinely need different implementations per target. Declare the contract once in , implement it once per target.
commonMainkotlin
// 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)
}expectactualactual当您确实需要为每个目标提供不同实现时,KMP的“逃生舱”机制。在中声明一次契约,为每个目标实现一次。
commonMainkotlin
// 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)
}expectactualactualexpectexpect
/actual
for composables
expectactual用于可组合项的expect
/actual
expectactualComposables follow the same rules. Useful when a feature needs a platform-specific Compose API (Android , iOS , Desktop ).
RuntimeShaderUIKitViewSwingPanelkotlin
// 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.systemMaterial()) },
modifier = Modifier.matchParentSize()
)
content()
}
}Rule: composables should be the exception, not the rule. Most "platform feel" differences can be tuned via tokens (colors, corner radii, spring stiffness) in , not via separate code paths.
expectcommonMain可组合项遵循相同的规则。当某个功能需要特定平台的Compose API(如Android的、iOS的、Desktop的)时非常有用。
RuntimeShaderUIKitViewSwingPanelkotlin
// commonMain
@Composable
expect fun PlatformBlur(modifier: Modifier = Modifier, content: @Composable () -> Unit)
// androidMain(在Android 13+上使用RuntimeShader)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier.graphicsLayer { renderEffect = blurEffect }) { content() }
}
// iosMain(通过UIKitView使用UIVisualEffectView)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
Box(modifier) {
UIKitView(
factory = { UIVisualEffectView(effect = UIBlurEffect.systemMaterial()) },
modifier = Modifier.matchParentSize()
)
content()
}
}规则:可组合项应是例外情况,而非常态。大多数“平台风格”差异可以通过中的标记(颜色、圆角半径、弹簧刚度)进行调整,而非通过单独的代码路径。
expectcommonMainLocalDensity
cross-platform
LocalDensity跨平台的LocalDensity
LocalDensityOn Android, reflects the device DPI bucket (1.0, 1.5, 2.0, 3.0...). On iOS, density is computed from (typically 2.0 or 3.0 on Retina). On Desktop, density depends on the screen scaling factor (1.0 by default; 2.0 on Retina-class displays; user-configurable on Windows). On Wasm, density follows .
LocalDensity.current.densityUIScreen.scalewindow.devicePixelRatioDon't hardcode to pixel ratios; trust and to handle conversion. If you need an exact pixel value (e.g., for a draw operation), do the conversion explicitly:
DpDpLocalDensityCanvaskotlin
val density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }Avoid reading inside hot loops; cache the conversion.
density在Android上,反映设备的DPI等级(1.0、1.5、2.0、3.0...)。在iOS上,密度由计算得出(Retina屏幕通常为2.0或3.0)。在Desktop上,密度取决于屏幕缩放因子(默认1.0;Retina级显示器为2.0;Windows上可由用户配置)。在Wasm上,密度遵循。
LocalDensity.current.densityUIScreen.scalewindow.devicePixelRatio不要将硬编码为像素比率;请信任和来处理转换。如果您需要精确的像素值(例如,用于绘制操作),请显式进行转换:
DpDpLocalDensityCanvaskotlin
val density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }避免在热循环中读取;请缓存转换结果。
densityLocalConfiguration
and platform-aware UI
LocalConfigurationLocalConfiguration
与平台感知UI
LocalConfigurationLocalConfiguration.currentandroidMain- - the window/screen size as
LocalWindowInfo.current.containerSize, available inIntSize.commonMain - - density, available in
LocalDensity.current.commonMain - - LTR / RTL.
LocalLayoutDirection.current - - read
BoxWithConstraints { ... }/maxWidthdirectly inside layout.maxHeight
If you need real device characteristics (orientation, idiom, model), wrap the access in / and pass a typed object like to the common layer.
expectactualPlatformInfoLocalConfiguration.currentandroidMain- - 窗口/屏幕尺寸,类型为
LocalWindowInfo.current.containerSize,可在IntSize中使用。commonMain - - 密度,可在
LocalDensity.current中使用。commonMain - - 从左到右(LTR)/从右到左(RTL)。
LocalLayoutDirection.current - - 在布局中直接读取
BoxWithConstraints { ... }/maxWidth。maxHeight
如果您需要真实的设备特性(方向、设备类型、型号),请将访问逻辑包装在/中,并将类型化对象(如)传递到共享层。
expectactualPlatformInfoFonts cross-platform via Compose Resources
通过Compose Resources实现跨平台字体
org.jetbrains.compose.resourcescommonMain/composeResources/font/RescomposeApp/src/commonMain/composeResources/
├── font/
│ ├── Inter-Regular.ttf
│ └── Inter-Bold.ttf
├── drawable/
│ └── logo.svg
└── values/
├── strings.xml ← default locale
└── strings.fr.xml ← French overridesUsage in :
commonMainkotlin
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)Same pattern for (image), (localized string via ), (raw bytes via ).
Res.drawable.logoRes.string.app_namestringResource(...)Res.file.configRes.readBytes(...)org.jetbrains.compose.resourcescommonMain/composeResources/font/RescomposeApp/src/commonMain/composeResources/
├── font/
│ ├── Inter-Regular.ttf
│ └── Inter-Bold.ttf
├── drawable/
│ └── logo.svg
└── values/
├── strings.xml ← 默认语言环境
└── strings.fr.xml ← 法语覆盖在中的用法:
commonMainkotlin
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.logoRes.string.app_namestringResource(...)Res.file.configRes.readBytes(...)iOS interop with SwiftUI
与SwiftUI的iOS互操作
CMP produces a you can drop into a SwiftUI app. KMP generates a top-level Kotlin function (commonly named or ) that returns a . Wrap it with .
UIViewControllerMainViewController()ComposeUIViewController { ... }UIViewControllerUIViewControllerRepresentablekotlin
// iosMain/kotlin/main.ios.kt
fun MainViewController(): UIViewController = ComposeUIViewController {
AppContent() // commonMain composable
}swift
// 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() }
}The Kotlin function name gets mangled to because the file is . Check the generated framework headers if the symbol name surprises you.
Main_iosKt.MainViewController()main.ios.ktCMP会生成一个,您可以将其嵌入到SwiftUI应用中。KMP会生成一个顶层Kotlin函数(通常命名为或),返回一个。使用进行包装。
UIViewControllerMainViewController()ComposeUIViewController { ... }UIViewControllerUIViewControllerRepresentablekotlin
// iosMain/kotlin/main.ios.kt
fun MainViewController(): UIViewController = ComposeUIViewController {
AppContent() // commonMain中的可组合项
}swift
// iOS应用目标
import SwiftUI
import ComposeApp // KMP生成的框架
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.ios.ktMain_iosKt.MainViewController()Android entry point
Android入口点
No interop ceremony on Android. The Activity hosts the common composable directly via .
setContent { ... }kotlin
// androidApp/src/main/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppContent() // commonMain composable
}
}
}If you need to pass or into , expose it via a DI graph or an in with in .
ContextActivitycommonMainexpect class PlatformContextcommonMainactual class PlatformContext(val context: Context)androidMainAndroid无需互操作仪式。Activity直接通过托管共享可组合项。
setContent { ... }kotlin
// androidApp/src/main/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppContent() // commonMain中的可组合项
}
}
}如果您需要将或传递到中,请通过DI图或在中暴露,并在中实现。
ContextActivitycommonMaincommonMainexpect class PlatformContextandroidMainactual class PlatformContext(val context: Context)Embedding SwiftUI/UIKit inside a Compose iOS view (the reverse direction)
在Compose iOS视图中嵌入SwiftUI/UIKit(反向操作)
Use for a factory or for a factory.
UIKitViewUIViewUIKitViewControllerUIViewControllerkotlin
// iosMain
UIKitView(
factory = {
UISwitch().apply {
addTarget(target, action = NSSelectorFromString("onToggle:"), forControlEvents = UIControlEventValueChanged)
}
},
modifier = Modifier.size(48.dp, 32.dp)
)For SwiftUI views: wrap them in a exposed via a Swift bridge function, then call from Kotlin via the generated headers (cinterop). See for the full pattern.
UIHostingController@objcreferences/cmp-interop.md对于工厂,使用;对于工厂,使用。
UIViewUIKitViewUIViewControllerUIKitViewControllerkotlin
// iosMain
UIKitView(
factory = {
UISwitch().apply {
addTarget(target, action = NSSelectorFromString("onToggle:"), forControlEvents = UIControlEventValueChanged)
}
},
modifier = Modifier.size(48.dp, 32.dp)
)对于SwiftUI视图:将它们包装在通过Swift 桥接函数暴露的中,然后通过生成的头文件(cinterop)从Kotlin调用。完整模式请参考。
@objcUIHostingControllerreferences/cmp-interop.mdAnimation cross-platform
跨平台动画
All animation APIs (, , , ) work identically across targets in CMP 1.7+. Spring tuning written in produces the same physics on Android and iOS. Gestures (, ) work cross-platform with the same API surface.
animate*AsStateAnimatedVisibilityupdateTransitionSharedTransitionLayoutcommonMainModifier.draggableModifier.pointerInputThe animation primer lives in . Cross-platform deltas to keep in mind:
../compose-motion/SKILL.md- iOS first-frame is slower (Skia bootstrap); a 200ms enter animation feels tighter on Android, slightly delayed on iOS cold start.
- Wasm motion can stutter on first frame (JIT warmup); pre-warm critical paths or hide motion until interactive.
所有动画API(、、、)在CMP 1.7+中跨目标的工作方式完全相同。在中编写的弹簧调整参数会在Android和iOS上产生相同的物理效果。手势(、)跨平台使用相同的API表面。
animate*AsStateAnimatedVisibilityupdateTransitionSharedTransitionLayoutcommonMainModifier.draggableModifier.pointerInput动画入门内容请参考。需要注意的跨平台差异:
../compose-motion/SKILL.md- iOS第一帧速度较慢(Skia初始化);200ms的入场动画在Android上感觉紧凑,在iOS冷启动时略有延迟。
- Wasm动画在第一帧可能出现卡顿(JIT预热);预热关键路径或在交互前隐藏动画。
What does NOT work (gotchas)
无法正常工作的内容(注意事项)
- Drawer state on iOS: native swipe-to-open from the leading edge conflicts with iOS's back-swipe gesture. Use a button trigger or move the swipe area inward 30dp+.
ModalNavigationDrawer - quirks: Android handles RTL natively, iOS Compose had bugs in 1.6 (text alignment, padding inversions). Improved in 1.7+ but verify with real Arabic/Hebrew strings.
LayoutDirection.Rtl - Soft keyboard handling: works on Android out of the box. On iOS Compose 1.6+, it requires
imePadding()setup or aIOSKeyboardEventListenerobserver wired through the platform layer.WindowInsets - does not exist in Compose. Use
Color.parseHex(...)or write a tiny extension.Color(0xFFRRGGBB) - System fonts on iOS via Compose: do not fallback to and expect SF Pro. Compose on iOS ships its own font fallback chain. Either bundle SF Pro via Compose Resources (license-permitting) or use
FontFamily.SansSerifto drop a nativeUIKitViewfor system-font text.UILabel - Animations on Web (Wasm): heavier startup, occasional first-frame stutter; profile with browser devtools and lazy-load heavy animation graphs.
- ,
java.util.UUIDand other JVM-only APIs are forbidden injava.io.Fileif you ship to iOS or Wasm. UsecommonMain,kotlinx.uuid, or thekotlinx-iomultiplatform port.okio
- iOS上的抽屉状态:原生从左侧边缘滑动打开的手势与iOS的返回滑动手势冲突。使用按钮触发或将滑动区域向内移动30dp以上。
ModalNavigationDrawer - 的问题:Android原生支持RTL,iOS Compose在1.6版本中存在bug(文本对齐、内边距反转)。1.7+版本有所改进,但需使用真实的阿拉伯语/希伯来语文本进行验证。
LayoutDirection.Rtl - 软键盘处理:在Android上开箱即用。在iOS Compose 1.6+中,需要设置
imePadding()或通过平台层关联IOSKeyboardEventListener观察者。WindowInsets - ****在Compose中不存在。使用
Color.parseHex(...)或编写一个小型扩展函数。Color(0xFFRRGGBB) - 通过Compose使用iOS系统字体:不要依赖并期望获得SF Pro字体。iOS上的Compose有自己的字体回退链。要么通过Compose Resources捆绑SF Pro(需符合许可),要么使用
FontFamily.SansSerif嵌入原生UIKitView来显示系统字体文本。UILabel - Web(Wasm)上的动画:启动负载较重,偶尔第一帧卡顿;使用浏览器开发者工具分析性能,并通过延迟组合来懒加载次要屏幕。
kotlinx.coroutines - **、
java.util.UUID**等仅JVM可用的API在java.io.File中是禁止的,如果您要发布到iOS或Wasm。请使用commonMain、kotlinx.uuid或kotlinx-io的多平台移植版本。okio
CMP version notes
CMP版本说明
- Compose Multiplatform 1.7 stable: cross-platform, improved iOS keyboard handling, lifecycle observability via
SharedTransitionLayouton iOS.LocalLifecycleOwner - Kotlin 2.0+ required (K2 compiler).
- Some Material 3 components have platform-specific look (e.g., on iOS auto-renders with iOS-style proportions;
Switchstays Material across all targets).DatePicker - plugin is the standard for assets; the older
compose-multiplatform-resourcesis no longer recommended for new projects.moko-resources
- Compose Multiplatform 1.7稳定版:支持跨平台,改进了iOS键盘处理,iOS上可通过
SharedTransitionLayout观察生命周期。LocalLifecycleOwner - 需要Kotlin 2.0+(K2编译器)。
- 部分Material 3组件具有特定平台的外观(例如,iOS上的会自动渲染为iOS风格的比例;
Switch在所有目标上保持Material风格)。DatePicker - 插件是资源管理的标准;旧版
compose-multiplatform-resources不再推荐用于新项目。moko-resources
Performance considerations
性能考虑
- iOS first-frame is slower than Android (Skia bootstrapping ~150-300ms cold). Keep your splash visible until the first composition emits, or pre-warm with a transparent root composable.
- Wasm bundle size: aim for <2MB compressed. Tree-shake heavy deps, lazy-load secondary screens via deferred composition, and inspect the
kotlinx.coroutinesoutput in.wasm.wasmJsBrowserDistribution - Desktop: cold start is fast on JVM; AOT compilation via Kotlin/Native is overkill for desktop unless you need a single-file binary.
- Android: same baseline as Jetpack Compose - profile with the Compose compiler stability metrics and recomposition counts.
Layout Inspector
- iOS第一帧比Android慢(Skia初始化约150-300ms冷启动)。保持闪屏可见直到第一次组合完成,或使用透明根可组合项进行预热。
- Wasm包大小:目标压缩后小于2MB。摇树优化重型依赖,通过延迟组合懒加载次要屏幕,并在
kotlinx.coroutines中检查wasmJsBrowserDistribution输出。.wasm - Desktop:JVM冷启动速度快;除非需要单文件二进制,否则通过Kotlin/Native进行AOT编译对于桌面来说是过度优化。
- Android:与Jetpack Compose基线相同——使用Compose编译器稳定性指标和重组计数进行性能分析。
Layout Inspector
Anti-Patterns
反模式
| BAD | GOOD | Why |
|---|---|---|
Reflection trick or | | Reflection breaks on Wasm/Native; |
Assuming Android | Inject a typed dependency via | |
| Hardcoding Material colors that look great on Android but jarring on iOS | Define a | Cross-platform consistency is good, but iOS users notice when a Material blue feels alien on iPhone |
| | Tight coroutine loops drain battery on iOS; infinite transitions pause when offscreen |
| 错误做法 | 正确做法 | 原因 |
|---|---|---|
在 | 使用 | 反射在Wasm/Native上会失效; |
假设Android的 | 通过 | |
| 硬编码在Android上美观但在iOS上不协调的Material颜色 | 在 | 跨平台一致性是好事,但iOS用户会注意到Material蓝色在iPhone上显得格格不入 |
在 | 使用 | 紧密的协程循环会消耗iOS电池;无限过渡在屏幕外时会暂停 |
Quick Reference: Loading Sub-skills
快速参考:加载子技能
| Need | Load |
|---|---|
| iOS / Android interop deep-dive | |
| Per-platform behavior catalog | |
| Animation API | |
| Advanced graphics (M3 Expressive, AGSL on Android only) | |
| iOS-side native interop with SwiftUI | |
| Mobile UX context | |
| Desktop UX context | |
| Foundation | |
| 需求 | 加载内容 |
|---|---|
| iOS / Android互操作深入讲解 | |
| 各平台行为目录 | |
| 动画API | |
| 高级图形(M3 Expressive、仅Android可用的AGSL) | |
| 与SwiftUI的iOS原生互操作 | |
| 移动UX背景 | |
| 桌面UX背景 | |
| 基础内容 | |
Sources
资料来源
- Meet-Miyani/compose-skill (KMP/CMP comprehensive)
- JetBrains Compose Multiplatform
- Compose Multiplatform docs
- skydoves/Orbital (KMP-aware shared transitions)
- Meet-Miyani/compose-skill(KMP/CMP综合资料)
- JetBrains Compose Multiplatform
- Compose Multiplatform文档
- skydoves/Orbital(支持KMP的共享过渡)