🔷 Compose Multiplatform 简介
Compose Multiplatform(CMP)是 JetBrains 在 KMP 基础上构建的跨平台 UI 框架,基于 Jetpack Compose API,支持在 Android、iOS、Desktop(JVM)和 Web(Wasm)上共享 UI 代码。
CMP 与 KMP 的关系: KMP 共享业务逻辑(必选),CMP 在此基础上还共享 UI 层(可选)。一个项目可以同时使用 KMP(共享业务逻辑)和 CMP(共享部分 UI 组件),也可以选择各平台用自己的 UI 框架。
9.1 CMP vs 原生 UI
| 维度 | Compose Multiplatform | 原生 UI (Compose/SwiftUI) |
|---|---|---|
| 代码复用率 | UI + 业务逻辑全部共享 | 只共享业务逻辑 |
| 平台原生感 | 高度一致,但非100%原生 | 完全原生 |
| iOS 稳定性 | Beta(CMP 1.6+) | 稳定 |
| 平台特性访问 | 需要 expect/actual | 直接访问 |
| 设计语言 | Material Design 为主 | 各平台 HIG 规范 |
| 适用场景 | B端/工具类应用,UI 一致性要求高 | 追求极致平台体验的 C 端应用 |
9.2 支持平台与状态
🤖 Android — Stable
完整支持,与 Jetpack Compose 完全一致,生产就绪。
🍎 iOS — Beta (CMP 1.6)
基于 Skiko 渲染引擎,大部分 API 可用,可访问 UIKit 组件,正在快速成熟中。
🖥️ Desktop — Stable
macOS / Windows / Linux 完整支持,可访问文件系统和原生菜单。
🌐 Web (Wasm) — Alpha
通过 WebAssembly 在浏览器中运行,性能良好,API 覆盖度持续提升。
📁 composeApp 模块结构
composeApp 目录结构
composeApp/src/
├── commonMain/ # 所有平台共享的 UI 代码
│ └── kotlin/
│ └── com/example/newsapp/
│ ├── App.kt # 根 Composable,AppTheme + NavHost
│ ├── ui/
│ │ ├── home/
│ │ │ ├── HomeScreen.kt
│ │ │ └── ArticleCard.kt
│ │ ├── detail/
│ │ │ └── ArticleDetailScreen.kt
│ │ └── components/ # 可复用 UI 组件
│ │ ├── LoadingView.kt
│ │ └── ErrorView.kt
│ └── theme/
│ ├── AppTheme.kt
│ └── Color.kt
│
├── androidMain/ # Android 入口
│ └── kotlin/
│ └── MainActivity.kt
│
└── iosMain/ # iOS Compose 入口(通过 MainViewController 暴露)
└── kotlin/
└── MainViewController.kt
9.3 App.kt — 根 Composable
commonMain — App.kt
@Composable
fun App() {
AppTheme {
val navController = rememberNavController()
AppNavGraph(navController)
}
}
9.4 Android 入口
androidMain — MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
9.5 iOS 入口
iosMain — MainViewController.kt
import androidx.compose.ui.window.*
import platform.UIKit.*
// 返回 UIViewController,在 Swift 端嵌入
fun MainViewController(): UIViewController =
ComposeUIViewController { App() }
iosApp/ContentView.swift — Swift 调用
import SwiftUI
import composeApp // 注意:CMP 模式下 import 的是 composeApp
struct ContentView: View {
var body: some View {
// 将 Compose UIViewController 嵌入 SwiftUI
ComposeView()
.ignoresSafeArea(.keyboard)
}
}
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
🔧 平台差异处理
9.6 WindowInsets 处理
commonMain
@Composable
fun HomeScreen() {
Scaffold(
// windowInsetsPadding 在 CMP 中跨平台有效
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing),
topBar = {
TopAppBar(title = { Text("每日资讯") })
}
) { innerPadding ->
ArticleList(modifier = Modifier.padding(innerPadding))
}
}
9.7 expect/actual UI 组件
commonMain — 平台特定 UI
// 某些 UI 行为需要平台特定实现
@Composable
expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit)
// 分享功能
expect fun shareText(text: String)
// 状态栏颜色
@Composable
expect fun SystemUiController(darkIcons: Boolean)
androidMain — BackHandler actual
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
// Android 有返回按钮,需要处理
androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack)
}
actual fun shareText(text: String) {
// Android 分享 Intent(需要 Context)
val context = androidContext()
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = "text/plain"
context.startActivity(Intent.createChooser(intent, null))
}
iosMain — BackHandler actual
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
// iOS 没有返回按钮,这里是空实现
// 导航返回由 iOS 的边缘滑动手势处理
}
actual fun shareText(text: String) {
val controller = UIActivityViewController(
activityItems = listOf(text),
applicationActivities = null
)
UIApplication.sharedApplication.keyWindow
?.rootViewController
?.presentViewController(controller, animated = true, completion = null)
}
9.8 平台特定字体
commonMain — 平台字体 expect
expect fun platformFontFamily(): FontFamily
androidMain / iosMain
// androidMain
actual fun platformFontFamily(): FontFamily = FontFamily.Default
// iosMain(使用系统 SF Pro 字体)
actual fun platformFontFamily(): FontFamily = FontFamily(Font(".AppleSystemUIFont"))
🎁 资源共享(composeResources)
9.11 资源目录结构
资源目录
composeApp/src/commonMain/composeResources/
├── drawable/
│ ├── logo.svg
│ └── placeholder.png
├── font/
│ ├── Roboto-Regular.ttf
│ └── Roboto-Bold.ttf
└── values/
└── strings.xml
9.12 字符串资源
values/strings.xml
<resources>
<string name="app_name">每日资讯</string>
<string name="home_title">首页</string>
<string name="bookmarks_title">收藏</string>
<string name="loading_text">加载中...</string>
<string name="error_retry">重试</string>
</resources>
commonMain — 使用字符串资源
import newsapp.composeapp.generated.resources.*
import org.jetbrains.compose.resources.*
@Composable
fun HomeScreen() {
Text(stringResource(Res.string.home_title))
}
9.13 图片和字体资源
commonMain — 使用图片和字体
// 图片
@Composable
fun AppLogo() {
Image(
painter = painterResource(Res.drawable.logo),
contentDescription = stringResource(Res.string.app_name)
)
}
// 自定义字体
val RobotoFontFamily = FontFamily(
Font(Res.font.Roboto_Regular),
Font(Res.font.Roboto_Bold, weight = FontWeight.Bold)
)
🚀 完整跨平台应用实战
9.14 CMP 主题(跨平台兼容)
commonMain — AppTheme.kt
private val AppColorScheme = darkColorScheme(
primary = Color(0xFF7F52FF),
onPrimary = Color.White,
surface = Color(0xFF13101E),
background = Color(0xFF0D0B14),
onBackground = Color(0xFFE8E4F0)
)
@Composable
fun AppTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = AppColorScheme,
content = content
)
}
9.15 BottomNavigation(跨平台)
commonMain — 带底部导航栏的 App
@Composable
fun App() {
AppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = currentRoute?.contains("HomeRoute") == true,
onClick = { navController.navigate(HomeRoute) },
icon = { Icon(Icons.Outlined.Home, null) },
label = { Text("首页") }
)
NavigationBarItem(
selected = currentRoute?.contains("BookmarksRoute") == true,
onClick = { navController.navigate(BookmarksRoute) },
icon = { Icon(Icons.Outlined.Bookmark, null) },
label = { Text("收藏") }
)
}
}
) { innerPadding ->
AppNavGraph(
navController = navController,
modifier = Modifier.padding(innerPadding)
)
}
}
}
🎯 实践任务
- 在 composeApp 的 commonMain 中创建
HomeScreen.kt,消费共享 ViewModel - 配置 composeResources,添加应用名称字符串资源
- 实现类型安全的 Navigation(使用
@Serializable路由) - 添加
BackHandler的 expect/actual,处理 Android 返回键行为 - 在 iOS 端的 ContentView.swift 中嵌入
ComposeView