mcpField
C
compose-multiplatform

compose-multiplatform

github.com/athevon开发698947 次安装3 周前更新
代码仓库

关于 compose-multiplatform

为 Kotlin Multiplatform 与 Compose Multiplatform 项目提供跨平台 UI 开发指导,涵盖 Android、iOS、桌面和 Web。适合需要共享 Compose 界面并处理平台差异的开发者。

主要功能

  1. 01区分 KMP 与 CMP 的职责
  2. 02规划共享与平台专属代码
  3. 03使用 expect/actual 实现平台差异
  4. 04共享字体、图片与字符串资源
  5. 05在 Compose 与 SwiftUI 间嵌入视图
  6. 06跨平台动画与手势注意事项

适用场景

  1. 01搭建多平台 Compose 项目结构
  2. 02将平台 API 接入共享界面
  3. 03在 iOS 应用中托管 Compose 界面
  4. 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

来源

评论

支持 Markdown