← 返回学习路线
🔷

第 9 章:Compose Multiplatform 共享 UI

用 Compose Multiplatform 在 Android、iOS 和桌面端共享 UI 代码,一次编写,多端运行

🔷 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) ) } } }

🎯 实践任务

  1. 在 composeApp 的 commonMain 中创建 HomeScreen.kt,消费共享 ViewModel
  2. 配置 composeResources,添加应用名称字符串资源
  3. 实现类型安全的 Navigation(使用 @Serializable 路由)
  4. 添加 BackHandler 的 expect/actual,处理 Android 返回键行为
  5. 在 iOS 端的 ContentView.swift 中嵌入 ComposeView