从 Android 组件化到 KMP 跨平台底座(九):KMP 与 CMP 的关系——到底要不要共享 UI?
前言
前面八篇,我们已经完成了从 Android 组件化到 KMP 跨平台业务底座的完整推演:
Android 组件化
│
▼
识别纯 Kotlin 业务逻辑
│
▼
迁入 commonMain
│
▼
建立 androidMain / iosMain 平台适配
│
▼
sharedLogic 分层
│
▼
KMP 多模块组件化
走到这里,Android、iOS 甚至 Web 已经可以共享:
-
数据模型
-
网络请求
-
Repository
-
UseCase
-
错误处理
-
Token 管理
-
二维码解析
-
地图点位模型
-
巡检表单校验
-
业务状态
但是,页面 UI 仍然有几种不同选择:
Android
使用 Jetpack Compose
iOS
使用 SwiftUI / UIKit
Web
使用 Vue / React
这时就会出现系列最后一个关键问题:
业务逻辑已经通过 KMP 共享了,UI 到底还要不要继续共享?
如果继续共享 UI,就会进入:
Compose Multiplatform
也就是 CMP。
但 CMP 并不是 KMP 的必选项。
一个项目可以只使用 KMP,也可以使用 KMP + CMP,还可以使用 KMP + Flutter。
所以这一篇不是简单介绍 Compose Multiplatform 怎么写页面,而是要解决以下问题:
-
KMP 和 CMP 到底是什么关系?
-
使用 KMP 后,是否必须使用 CMP?
-
Android Compose 页面能不能直接迁到 CMP?
-
哪些页面适合共享?
-
哪些页面不应该强行共享?
-
sharedLogic和sharedUI应该怎么划分? -
扫码、地图、相机如何接入共享 UI?
-
CMP 与 SwiftUI、UIKit、Android View 能不能混合?
-
KMP + CMP 与 KMP + Flutter 应该怎么选择?
-
设备巡检项目最终应该采用哪种路线?
一、先说结论:KMP 不等于 CMP
KMP 的全称是:
Kotlin Multiplatform
它解决的是:
Kotlin 代码如何在 Android、iOS、Web、Desktop 等多个平台之间共享。
CMP 的全称是:
Compose Multiplatform
它解决的是:
Compose UI 如何在多个平台之间共享。
因此,两者的关系是:
KMP
是跨平台基础能力
CMP
是建立在 KMP 之上的跨平台 UI 框架
可以用一个简单公式表示:
KMP
= 跨平台代码共享能力
KMP + CMP
= 跨平台业务逻辑 + 跨平台 Compose UI
Compose Multiplatform 使用 Kotlin Multiplatform 作为基础,让 Compose UI 可以运行在 Android、iOS、Desktop 和 Web 等目标上。按照当前官方状态,Compose Multiplatform 在 Android、iOS 和 Desktop 上已经处于稳定状态,Web 端目前仍为 Beta。
所以:
使用 CMP
一定是在使用 KMP
使用 KMP
不一定要使用 CMP
二、只使用 KMP,不使用 CMP 完全没问题
纯 KMP 项目可以采用:
sharedLogic
负责公共业务逻辑
androidApp
使用 Compose / XML
iosApp
使用 SwiftUI / UIKit
webApp
使用 Vue / React
整体结构如下:
sharedLogic
│
┌─────────────┼─────────────┐
│ │ │
Android UI iOS UI Web UI
Compose/XML SwiftUI/UIKit Vue/React
例如设备列表业务:
sharedLogic
├── Device
├── DeviceRepository
├── GetDeviceListUseCase
└── DeviceListState
Android 使用 Compose:
@Composable
fun AndroidDeviceListScreen(
state: DeviceListState,
onDeviceClick: (String) -> Unit
) {
// Android Compose 页面
}
iOS 使用 SwiftUI:
struct DeviceListView: View {
let state: DeviceListState
var body: some View {
// SwiftUI 页面
}
}
两端 UI 分别实现,但共用:
-
Device
-
DeviceListState
-
Repository
-
UseCase
-
网络请求
-
错误处理
这种路线的优点是:
-
UI 完全符合平台习惯
-
Android 和 iOS 可以独立设计
-
原生 SDK 接入自然
-
平台最新能力可以及时使用
-
不需要迁移已有页面
缺点是:
-
页面仍然要写两份
-
UI 逻辑可能重复
-
Android 和 iOS 页面行为容易不一致
-
需要同时维护 Compose 和 SwiftUI 团队能力
所以:
只共享业务逻辑,已经是一套完整且合理的 KMP 方案。
KMP 官方也明确支持开发者保留原生 UI,或者逐步将部分页面替换为 Compose Multiplatform,而不是要求整个项目一次性共享全部 UI。
三、KMP + CMP 又是什么结构?
如果决定继续共享 UI,可以新增:
sharedUI
项目结构可以是:
project/
├── sharedLogic/
│ ├── commonMain/
│ ├── androidMain/
│ └── iosMain/
│
├── sharedUI/
│ ├── commonMain/
│ ├── androidMain/
│ └── iosMain/
│
├── androidApp/
├── iosApp/
└── webApp/
职责分别是:
sharedLogic
负责业务逻辑
sharedUI
负责可共享 Compose UI
androidApp
负责 Android 入口和 Android 平台能力
iosApp
负责 iOS 入口和 iOS 平台能力
webApp
负责 Web 入口和 Web 平台能力
依赖方向:
androidApp / iosApp / webApp
│
▼
sharedUI
│
▼
sharedLogic
不能反过来:
sharedLogic
不能依赖 sharedUI
因为业务层不应该知道页面最终使用:
-
Compose
-
SwiftUI
-
Flutter
-
Vue
-
React
四、KMP + CMP 的核心价值是什么?
如果只看表面,CMP 的价值似乎只是:
Android 和 iOS 少写一套页面
实际上,它带来的共享范围更广。
可以共享:
-
页面布局
-
通用组件
-
页面状态渲染
-
表单交互
-
列表展示
-
动画
-
主题
-
字体样式
-
加载状态
-
空状态
-
错误页面
-
部分导航
-
页面事件处理
-
UI 自动化测试思路
例如设备卡片:
@Composable
fun DeviceCard(
device: DeviceUiModel,
onClick: () -> Unit
) {
Card(
onClick = onClick
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = device.name,
style = MaterialTheme
.typography
.titleMedium
)
Spacer(
modifier = Modifier.height(8.dp)
)
Text(
text = device.statusText,
style = MaterialTheme
.typography
.bodyMedium
)
}
}
}
这段代码可以被 Android 和 iOS 页面共同使用。
对于 Android Compose 开发者来说,CMP 的学习成本相对可控,因为 Compose Multiplatform 与 Jetpack Compose 共享大量熟悉的布局、状态、动画和 Material API。官方也将其定位为 Android Compose 技能向其他平台的自然扩展。
五、Android Compose 页面能不能直接迁到 CMP?
答案是:
有些页面可以接近直接迁移,有些页面需要改造,有些页面不适合迁移。
不能简单地认为:
只要是 @Composable
就一定能直接放进 commonMain
判断依据仍然是:
页面是否依赖 Android 平台 API和仅支持 Android 的库?
第一类:比较容易迁移的 Compose 页面
例如:
-
登录页
-
设备列表
-
设备详情
-
普通表单
-
设置页面
-
个人中心
-
加载页面
-
空状态页面
-
错误页面
-
普通弹窗
-
卡片、按钮、列表项
这些页面通常主要使用:
Column
Row
Box
LazyColumn
Text
Button
Card
TextField
Image
State
动画
如果页面数据来自平台无关的 State,它们通常比较适合迁入:
sharedUI/commonMain
官方提供了从现有 Jetpack Compose 应用逐步迁移到 Kotlin Multiplatform 和 Compose Multiplatform 的完整指导,迁移可以覆盖业务逻辑、资源和 UI,而不是必须重建一个全新的项目。
第二类:改造后可以迁移的页面
例如页面中使用了:
LocalContext.current
Toast.makeText(...)
Intent(...)
ActivityResultLauncher
androidx.compose.ui.res.painterResource
android.net.Uri
这些能力绑定 Android 平台。
页面主体可能仍然能够共享,但需要把平台操作提取成回调或平台接口。
例如原 Android 页面:
@Composable
fun DeviceDetailScreen(
device: Device
) {
val context = LocalContext.current
Button(
onClick = {
Toast.makeText(
context,
"操作成功",
Toast.LENGTH_SHORT
).show()
}
) {
Text("提交")
}
}
改成公共页面:
@Composable
fun DeviceDetailScreen(
device: Device,
onSubmit: () -> Unit
) {
Button(
onClick = onSubmit
) {
Text("提交")
}
}
Android:
DeviceDetailScreen(
device = device,
onSubmit = {
Toast.makeText(
context,
"操作成功",
Toast.LENGTH_SHORT
).show()
}
)
iOS:
DeviceDetailScreen
│
▼
iOS 平台显示提示
共享 UI 不应该直接控制平台 Toast、Intent 或 ViewController。
第三类:不建议第一阶段强行迁移的页面
例如:
-
CameraX 相机页面
-
扫码预览页
-
高德地图页
-
WebView 容器
-
视频播放器
-
蓝牙设备控制页
-
系统文件选择器
-
强依赖 Activity 的页面
-
强依赖 UIKit Controller 的页面
-
复杂系统手势和平台导航页面
这些页面并不是永远不能和 CMP 结合。
但它们通常需要:
-
原生 View 嵌入
-
权限管理
-
生命周期处理
-
平台 SDK 适配
-
事件桥接
因此,第一阶段更稳妥的方式是:
普通页面共享
强平台页面继续平台化
六、哪些页面最适合使用 CMP?
可以把页面分成三档。
第一档:优先共享
典型页面:
登录页
注册页
设备列表
设备详情
巡检记录列表
普通巡检表单
个人中心
设置页
加载页
空状态
错误页
通用弹窗
这些页面通常具有:
-
数据驱动
-
平台差异少
-
交互逻辑统一
-
UI 结构接近
-
不依赖强平台 SDK
非常适合进入:
sharedUI/commonMain
第二档:共享外壳,平台实现核心区域
例如:
扫码页
地图页
图片选择页
视频播放页
WebView 页面
可以共享:
-
顶部标题
-
页面背景
-
提示文案
-
底部操作栏
-
加载状态
-
错误状态
-
操作按钮
平台侧实现:
-
相机预览
-
MapView
-
WebView
-
视频播放器
-
系统文件选择器
整体结构:
共享 Compose 页面外壳
├── 共享标题
├── 共享提示
├── 平台原生内容区域
└── 共享底部操作
Compose Multiplatform 支持将 Compose 嵌入 UIKit 或 SwiftUI,也支持在 Compose 页面中嵌入 UIKit、SwiftUI 等平台内容。
第三档:完全平台化
例如:
Android 前台服务控制页
iOS 系统专属页面
复杂高德地图页面
深度相机页面
系统设置页面
平台支付页面
平台级导航和系统 TabBar
如果平台差异明显,直接分别实现通常更加稳定。
共享 UI 不应该成为必须完成的 KPI。
七、共享 UI 不是共享全部平台行为
例如设备详情页可以共享:
设备名称
设备状态
基本信息
操作按钮
加载状态
但点击“拨打电话”后:
Android 可能调用:
Intent.ACTION_DIAL
iOS 可能调用:
UIApplication
共享页面只应该发出:
sealed interface DeviceDetailEvent {
data class CallPhone(
val phoneNumber: String
) : DeviceDetailEvent
data class OpenMap(
val deviceId: String
) : DeviceDetailEvent
data class StartScan(
val deviceId: String
) : DeviceDetailEvent
}
平台入口处理事件:
sharedUI
负责表达用户想做什么
androidApp / iosApp
负责决定平台上怎么做
这和前面讲的平台能力设计是一致的。
八、sharedLogic 和 sharedUI 应该如何依赖?
推荐方向:
sharedUI
依赖 sharedLogic
sharedLogic
不依赖 sharedUI
例如:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(
project(":sharedLogic")
)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
}
}
}
sharedLogic 提供:
Device
DeviceRepository
GetDeviceListUseCase
AppResult
AppError
sharedUI 提供:
DeviceListState
DeviceListEvent
DeviceListPresenter
DeviceListScreen
DeviceCard
平台 App 提供:
应用入口
导航宿主
权限
扫码
地图
平台 SDK
九、UI State 应该放在 sharedLogic 还是 sharedUI?
需要区分两种状态。
第一种:业务状态
例如:
enum class DeviceStatus {
ONLINE,
OFFLINE,
WARNING
}
sealed interface LoginState {
data object LoggedOut : LoginState
data class LoggedIn(
val user: User
) : LoginState
}
这些状态表达业务含义,应该放在:
sharedLogic
第二种:页面展示状态
例如:
data class DeviceListUiState(
val isLoading: Boolean = false,
val devices: List<DeviceUiModel> =
emptyList(),
val errorMessage: String? = null,
val selectedDeviceId: String? = null
)
它包含:
-
Loading
-
页面错误文案
-
选中项
-
展示模型
-
页面筛选条件
这些更适合放在:
sharedUI
或者单独拆成:
sharedPresentation
不要把页面专属状态全部塞进纯业务 sharedLogic。
否则以后不使用 CMP,而改用 Flutter 或 SwiftUI 时,业务底座会携带大量 Compose 页面概念。
十、要不要单独建立 sharedPresentation?
除了:
sharedLogic
sharedUI
还可以增加:
sharedPresentation
结构:
sharedLogic
负责 Domain 和 Data
sharedPresentation
负责 UI State、事件和状态管理
sharedUI
负责 Compose 页面
依赖方向:
sharedUI
│
▼
sharedPresentation
│
▼
sharedLogic
这样即使 iOS 不使用 CMP,也可以使用:
SwiftUI
│
▼
sharedPresentation
│
▼
sharedLogic
Flutter 也可以通过桥接使用相同 Presentation 状态。
但这种结构会增加 Module 数量和复杂度。
对于当前设备巡检 Demo,可以先采用:
sharedLogic
+
sharedUI
等确定需要“共享状态但不共享 UI”时,再拆 sharedPresentation。
十一、共享 ViewModel 还是只共享 UseCase?
主要有三种方案。
方案一:只共享 UseCase
sharedLogic
提供 UseCase
Android
自己写 ViewModel
iOS
自己写 ObservableObject
优点:
-
平台 UI 最自由
-
生命周期最自然
-
业务层边界清晰
缺点:
-
页面状态管理重复
-
加载、错误、刷新逻辑可能写两份
适合纯 KMP + 原生 UI。
方案二:共享页面状态管理
sharedLogic
提供 Repository / UseCase
sharedPresentation
提供 State 和事件处理
Android / iOS
只渲染 State
优点:
-
页面业务状态共享
-
UI 仍可原生实现
-
双端行为一致
缺点:
-
需要处理 Kotlin StateFlow 与 Swift 的消费方式
-
生命周期桥接增加
方案三:状态和 UI 全部共享
sharedLogic
负责业务
sharedUI
负责状态管理 + Compose 页面
优点:
-
共享率最高
-
Android 和 iOS 页面行为一致
-
单一团队维护成本较低
缺点:
-
平台差异处理更复杂
-
页面会更加依赖 CMP
-
iOS 团队的 SwiftUI 参与空间减少
对于 KMP + CMP 项目,可以优先使用第三种,但仍然要避免把业务规则写进 Composable。
十二、Composable 应该保持“无业务化”
不推荐:
@Composable
fun DeviceListScreen(
repository: DeviceRepository
) {
LaunchedEffect(Unit) {
repository.getDeviceList()
}
}
页面直接依赖 Repository,会导致:
-
UI 和 Data 耦合
-
测试困难
-
状态管理分散
-
生命周期逻辑混乱
更合理的是:
@Composable
fun DeviceListScreen(
state: DeviceListUiState,
onEvent: (DeviceListEvent) -> Unit
) {
when {
state.isLoading -> {
LoadingContent()
}
state.errorMessage != null -> {
ErrorContent(
message =
state.errorMessage,
onRetry = {
onEvent(
DeviceListEvent.Retry
)
}
)
}
else -> {
DeviceListContent(
devices = state.devices,
onDeviceClick = {
onEvent(
DeviceListEvent
.DeviceClick(it)
)
}
)
}
}
}
页面只负责:
State
→ UI
以及:
用户操作
→ Event
业务请求和规则由状态管理层处理。
十三、导航到底要不要共享?
导航也有两种路线。
路线一:导航全部共享
结构:
sharedUI
├── AppNavigation
├── LoginRoute
├── DeviceRoute
└── InspectionRoute
Android、iOS 都使用同一套 Compose 导航。
优点:
-
页面流程只维护一份
-
路由参数统一
-
页面转场逻辑共享
-
适合全 CMP App
Compose Multiplatform 当前已经提供适用于跨平台项目的 Compose Navigation 方案,可以在 Android、iOS、Desktop 和 Web 等目标上组织共享导航。
路线二:页面共享,导航平台化
结构:
sharedUI
负责单个 Screen 内容
Android
使用自己的导航宿主
iOS
使用 SwiftUI NavigationStack
共享页面通过回调发出导航事件:
@Composable
fun DeviceListScreen(
state: DeviceListUiState,
onOpenDevice: (String) -> Unit
)
Android:
onOpenDevice
→ Android Navigation
iOS:
onOpenDevice
→ SwiftUI NavigationStack
优点:
-
更容易接入已有原生项目
-
平台导航体验更自然
-
平台系统特性更容易使用
-
支持渐进式迁移
官方当前也提供了将 SwiftUI 作为原生导航外壳、Compose 继续负责页面内容的实践方案。这说明即使共享 UI,也不一定要共享最外层导航和系统级 UI。
推荐判断
全新 KMP + CMP 项目
可以考虑共享导航
已有 Android / iOS 原生项目
优先保留平台导航
强依赖系统 TabBar、导航栏和平台手势
优先平台导航
十四、资源能不能共享?
CMP 不仅可以共享 Kotlin UI 代码,也可以共享部分资源。
例如:
字符串
图片
图标
字体
颜色相关资源
原始文件
可以放在:
sharedUI/
└── src/
└── commonMain/
└── composeResources/
├── drawable/
├── font/
├── values/
└── files/
Compose Multiplatform 提供了公共资源库和 Gradle 支持,可以在公共代码中访问字符串、图片和字体等资源。
但是以下资源仍然需要平台配置:
AndroidManifest
Info.plist
Android App Icon
iOS App Icon
启动图
权限说明
推送配置
签名
平台 SDK Key
因此:
页面使用的普通资源
可以共享
应用系统资源和平台配置
继续平台化
十五、CMP 能不能调用 Android View 和 UIKit?
可以。
Compose Multiplatform 并不是一个封闭的 UI 世界。
可以在 Compose 中嵌入:
Android View
UIKit UIView
UIViewController
SwiftUI 内容
也可以将 Compose 页面嵌入:
Android 原生页面
UIKit 应用
SwiftUI 应用
官方文档已经提供 Compose 与 UIKit、SwiftUI 之间双向嵌入的集成方式。
这意味着:
使用 CMP
不等于必须放弃全部原生 UI
例如地图页可以设计为:
CMP 页面
├── Compose 顶部栏
├── Compose 筛选控件
├── 原生 MapView
└── Compose 底部设备卡片
扫码页可以设计为:
CMP 页面
├── Compose 标题
├── 原生相机预览
├── Compose 扫码框
└── Compose 提示文案
但需要注意:
能嵌入不等于一定值得嵌入。
如果生命周期和手势交互过于复杂,整页平台化可能更简单。
十六、扫码页面应该如何与 CMP 配合?
有三种方案。
方案一:扫码页完全平台化
共享设备列表
│
▼
点击扫码
│
▼
Android 打开 CameraX 页面
iOS 打开 AVFoundation 页面
│
▼
获得 rawContent
│
▼
commonMain ScanParser
优点:
-
生命周期最稳定
-
平台 SDK 接入自然
-
第一阶段最推荐
方案二:共享页面外壳
sharedUI
负责标题、提示、按钮和扫码框
平台 View
负责相机预览
优点:
-
UI 风格更加统一
-
一部分页面可以共享
缺点:
-
原生 View 嵌入复杂
-
权限和生命周期仍需平台处理
方案三:完全共享扫码页
需要使用跨平台相机和扫码能力,或者对各平台进行完整封装。
优点:
-
共享率高
缺点:
-
工程复杂度高
-
强依赖跨平台库能力
-
平台排错成本增加
对于设备巡检项目,推荐:
第一阶段使用方案一
稳定以后再考虑方案二
十七、地图页面应该如何与 CMP 配合?
地图同样有三种路线。
路线一:地图整页平台化
Android
高德地图 Compose / MapView
iOS
MapKit / 高德地图 iOS
KMP 共享:
GeoPoint
MapMarkerModel
GetDeviceMarkersUseCase
这是最稳妥的方案。
路线二:CMP 页面嵌入原生 MapView
共享:
-
页面标题
-
筛选条件
-
设备卡片
-
加载状态
-
错误状态
平台提供:
-
高德 MapView
-
MapKit View
这是共享率和平台能力之间的折中方案。
路线三:使用 CMP 地图库
如果项目选用的地图库已经提供成熟 KMP/CMP 支持,也可以进一步共享。
但需要评估:
-
平台功能完整度
-
地图厂商要求
-
中国坐标系
-
定位能力
-
Marker 聚合
-
路线规划
-
生命周期
-
商业授权
不要只因为“可以共享”,就替换已经稳定运行的地图 SDK。
十八、CMP 和 Flutter 有什么关系?
CMP 和 Flutter 都可以负责跨平台 UI。
区别主要在技术体系。
KMP + CMP
= Kotlin 业务底座
+
Compose UI
KMP + Flutter
= Kotlin 业务底座
+
Flutter UI
对比:
| 对比项 | KMP + CMP | KMP + Flutter |
|---|---|---|
| UI 语言 | Kotlin | Dart |
| UI 框架 | Compose | Flutter Widget |
| 业务底座 | KMP | KMP |
| Android Compose 技能复用 | 高 | 低 |
| KMP 调用链 | 直接 | 需要 Flutter 桥接 |
| 模型转换 | 较少 | Dart 与 Kotlin 转换 |
| 状态桥接 | Kotlin 内部 | Flow、Future、Stream 适配 |
| 技术栈数量 | 相对少 | Kotlin + Dart + 平台桥接 |
| 适合团队 | Kotlin/Compose 团队 | Flutter UI 团队 |
如果已经有成熟 Flutter UI 团队,KMP + Flutter 完全可行。
但如果团队核心是 Android Kotlin 和 Compose,KMP + CMP 通常更自然。
十九、CMP 的优势是什么?
1. Android Compose 能力可以继续复用
团队已有的:
-
Compose 布局经验
-
State 思维
-
Material 组件
-
动画
-
Modifier
-
Coroutine
-
Flow
-
Kotlin
都可以继续使用。
2. 减少 UI 重复开发
Android 和 iOS 不需要分别实现:
-
登录页
-
列表页
-
表单页
-
通用组件
-
加载页面
-
错误页面
3. 页面行为更容易保持一致
例如表单校验错误的位置、加载逻辑、按钮状态和空页面,都可以只维护一套。
4. 与 KMP 业务底座连接自然
调用链可以全部使用 Kotlin:
Composable
│
▼
State Holder
│
▼
UseCase
│
▼
Repository
不需要 Dart、Swift 和 Kotlin 之间大量模型转换。
5. 支持渐进式接入
CMP 可以从:
一个组件
一个页面
一个 Feature
逐步接入,而不是要求一次替换整个 App。官方也将渐进式采用和原生 UI 互操作作为 Compose Multiplatform 的主要能力之一。
二十、CMP 的代价是什么?
1. 仍然需要理解 iOS
使用 CMP 不代表可以完全忽略:
-
Xcode
-
Swift
-
UIKit
-
SwiftUI
-
Info.plist
-
签名
-
权限
-
iOS 生命周期
-
App Store 上架
CMP 共享的是 UI,不是消除 iOS 平台。
2. 平台体验需要主动设计
Android 和 iOS 的:
-
导航习惯
-
返回手势
-
TabBar
-
弹窗样式
-
字体
-
安全区
-
键盘
-
系统控件
并不完全相同。
共享 UI 如果只按照 Android 习惯设计,iOS 用户可能会感觉不自然。
3. 强平台能力仍然需要适配
例如:
-
地图
-
相机
-
扫码
-
WebView
-
支付
-
推送
-
蓝牙
-
系统分享
不会因为使用 CMP 就自动统一。
4. 跨平台依赖选择受到限制
Android 页面原来使用的某些 Compose 库可能只支持 Android。
迁到 commonMain 后,需要:
-
查找 KMP 版本
-
替换实现
-
留在
androidMain -
或自己做平台适配
5. 调试链路更长
一个 iOS 页面问题可能涉及:
Compose UI
Kotlin/Native
UIKit 互操作
Xcode
平台权限
平台 SDK
团队仍然需要具备跨平台排错能力。
二十一、不要追求百分之百 UI 共享
UI 共享率不应该成为唯一目标。
假设一个项目有十个页面:
登录
设备列表
设备详情
巡检表单
巡检记录
个人中心
地图
扫码
WebView
蓝牙调试
合理的共享方案可能是:
共享 6 个普通页面
平台化 4 个强平台页面
这并不代表 CMP 使用失败。
反而可能是成本最低、质量最高的方案。
真正需要关注的是:
-
是否减少重复工作
-
页面行为是否一致
-
平台体验是否保留
-
SDK 是否容易维护
-
团队是否容易排错
-
后续修改是否只需要改一处
所以:
共享率高
不一定等于架构好
二十二、设备巡检 Demo 的页面怎么分类?
设备巡检 Demo 包含:
登录
设备列表
设备详情
扫码
地图
巡检表单
巡检记录
个人中心
可以这样划分。
适合直接共享
登录页
设备列表
设备详情
巡检表单
巡检记录
个人中心
加载状态
错误页面
通用弹窗
这些放进:
sharedUI/commonMain
推荐平台化
扫码页面
地图页面
权限处理
WebView
Android:
CameraX
ML Kit
高德地图
Android 权限
iOS:
AVFoundation
MapKit
iOS 权限
可以共享的强平台页面外壳
扫码页可以共享:
标题
扫码提示
帮助按钮
扫码结果提示
地图页可以共享:
筛选栏
设备状态卡片
加载状态
地图错误提示
二十三、推荐的设备巡检项目结构
lift-kmp-core/
├── core-model/
├── core-result/
├── core-network/
├── core-storage/
├── core-platform/
│
├── feature-auth/
├── feature-device/
├── feature-inspection/
│
├── sharedLogic/
│
├── sharedUI/
│ └── src/
│ ├── commonMain/
│ │ ├── theme/
│ │ ├── components/
│ │ ├── auth/
│ │ ├── device/
│ │ └── inspection/
│ │
│ ├── androidMain/
│ └── iosMain/
│
├── android-qrcode/
├── android-map/
├── android-webview/
│
├── androidApp/
└── iosApp/
依赖方向:
androidApp
├── sharedUI
├── android-qrcode
├── android-map
└── android-webview
iosApp
├── sharedUI Framework
├── iOS Scanner
└── iOS Map
sharedUI
└── sharedLogic / feature-*
sharedLogic
└── feature-* / core-*
二十四、sharedUI 内部可以怎么组织?
sharedUI/src/commonMain/kotlin/
├── app/
│ ├── App.kt
│ └── AppNavigation.kt
│
├── theme/
│ ├── AppTheme.kt
│ ├── AppColors.kt
│ └── AppTypography.kt
│
├── components/
│ ├── AppButton.kt
│ ├── AppDialog.kt
│ ├── LoadingContent.kt
│ ├── ErrorContent.kt
│ └── EmptyContent.kt
│
├── auth/
│ ├── LoginScreen.kt
│ ├── LoginUiState.kt
│ └── LoginEvent.kt
│
├── device/
│ ├── DeviceListScreen.kt
│ ├── DeviceDetailScreen.kt
│ ├── DeviceCard.kt
│ ├── DeviceListUiState.kt
│ └── DeviceListEvent.kt
│
└── inspection/
├── InspectionFormScreen.kt
├── InspectionRecordScreen.kt
└── InspectionUiState.kt
这里仍然按照 Feature 组织,而不是:
screens/
states/
events/
components/
把所有业务页面混在一起。
推荐:
全局通用组件
放 components
业务页面和业务状态
放各自 Feature
二十五、迁移已有 Android Compose 项目的推荐顺序
不要一次把所有 Android Compose 页面移动到 commonMain。
第一步:先稳定 sharedLogic
先确保:
-
Repository
-
UseCase
-
网络
-
Token
-
错误
-
Model
已经跨平台运行。
第二步:迁移纯 UI 组件
例如:
AppButton
LoadingContent
ErrorContent
DeviceCard
StatusTag
这类组件风险最低。
第三步:迁移简单页面
例如:
登录页
设备列表
个人中心
第四步:迁移表单页面
表单涉及:
-
输入状态
-
键盘
-
校验
-
焦点
-
提交
需要 Android 和 iOS 真机验证。
第五步:决定导航是否共享
可以先保留平台导航,只共享 Screen。
等页面稳定后,再判断是否迁移整套导航。
第六步:最后处理强平台页面
例如:
地图
扫码
WebView
相机
这类页面不应成为第一批 CMP 迁移对象。
二十六、什么时候应该选择纯 KMP?
如果项目符合以下特点,更适合纯 KMP:
-
已有成熟 Android 和 iOS 原生项目
-
双端已有完整 UI 团队
-
平台 UI 差异明显
-
强平台页面很多
-
希望保留 SwiftUI 和 Compose
-
当前主要痛点是业务逻辑重复
-
不希望大规模重写页面
结构:
sharedLogic
+
Android Compose
+
iOS SwiftUI
二十七、什么时候应该选择 KMP + CMP?
如果项目符合以下特点,可以优先考虑 KMP + CMP:
-
Android 项目已经全面 Compose 化
-
iOS UI 尚未完整建设
-
团队主要是 Kotlin 开发者
-
普通列表和表单页面较多
-
Android 与 iOS 产品结构一致
-
希望减少双端 UI 重复
-
能接受部分平台页面单独实现
结构:
sharedLogic
+
sharedUI
+
平台能力适配
二十八、什么时候应该选择 KMP + Flutter?
如果项目符合以下特点,可以考虑 KMP + Flutter:
-
团队已有成熟 Flutter UI 能力
-
已经存在大量 Flutter 页面
-
希望 KMP 作为独立业务 SDK
-
KMP Core 还要提供给原生客户端
-
可以承担 Dart 与 Kotlin 桥接成本
-
UI 统一比 Kotlin 技术栈统一更重要
结构:
Flutter
负责 UI 和 Presentation Adapter
KMP
负责业务底座
平台层
负责 SDK 和系统能力
二十九、设备巡检项目最终推荐哪种路线?
结合这个系列的项目背景:
Android 已经全面使用 Compose
iOS 目前没有完整 UI
希望减少 Android 和 iOS 重复开发
项目包含大量列表、表单和设备业务
同时存在扫码、地图、定位等平台能力
更适合的路线是:
KMP
+
CMP
+
强平台能力平台化
具体分工:
sharedLogic
负责业务逻辑
sharedUI
负责普通 Compose 页面
androidApp / iosApp
负责平台入口
android-qrcode / iOS Scanner
负责扫码
android-map / iOS Map
负责地图
Platform Adapter
负责权限、定位和系统能力
也就是:
KMP Core
建议作为稳定底座
CMP
用于共享普通页面
扫码、地图、相机、WebView
继续平台化
三十、常见错误总结
错误一:认为用了 KMP 就必须用 CMP
正确理解:
KMP 可以独立存在
CMP 是可选 UI 方案
错误二:认为 Android Compose 页面可以全部直接复制
正确理解:
只使用跨平台 API 和跨平台依赖的页面才适合直接迁移。
错误三:sharedLogic 依赖 sharedUI
正确方向:
sharedUI
依赖 sharedLogic
错误四:Composable 直接调用 Repository
正确方式:
Composable
消费 State
发送 Event
错误五:为了共享 UI 强行统一所有导航
已有原生项目可以:
共享 Screen
保留平台 Navigation
错误六:把地图、相机和 WebView 全部强行 common 化
正确方式:
共享页面外壳和业务模型
平台实现核心 SDK
错误七:认为使用 CMP 后不再需要 Swift 和 Xcode
正确理解:
CMP 减少重复 UI,不会消除平台开发、签名、权限、SDK 和上架工作。
错误八:追求百分之百 UI 共享
正确目标:
共享有价值的页面
保留有价值的平台差异
三十一、整个系列最终形成的架构
经过九篇文章,最终得到:
平台 App
┌──────────────┼──────────────┐
│ │ │
androidApp iosApp webApp
│ │ │
├────平台能力───┼────平台能力───┤
│ │ │
└──────────────┼──────────────┘
│
sharedUI
│
sharedLogic
│
┌──────────────┼──────────────┐
│ │ │
feature-auth feature-device feature-inspection
│ │ │
└──────────────┼──────────────┘
│
┌─────────────┼─────────────┐
│ │ │
core-network core-storage core-platform
│ │ │
└─────────────┼─────────────┘
│
core-model/result
这里:
Module
划分业务职责
Source Set
划分平台边界
KMP
共享业务逻辑
CMP
共享可复用 UI
平台 App
负责应用入口
平台适配模块
负责 SDK、权限、硬件和生命周期
三十二、系列总结
这个系列不是单纯介绍 KMP 语法。
它真正想解决的是:
一个已经完成 Android 组件化的项目,应该如何继续演进成跨平台业务底座?
整个过程不是推倒重来,而是逐步升级。
第一步:
识别 Android 模块中的纯 Kotlin 逻辑
第二步:
把公共业务逻辑迁入 commonMain
第三步:
把 Android Library 重新定位为平台适配层
第四步:
通过 androidMain 和 iosMain 实现平台差异
第五步:
建立 model、result、network、
repository、usecase 分层
第六步:
拆分 core-* 和 feature-* KMP Module
第七步:
根据项目情况决定是否使用 CMP 共享 UI
最终不是为了得到一个 KMP Hello World,而是形成:
可复用
可测试
可扩展
可渐进式迁移
可服务多个平台
的跨平台业务底座。
可以用三句话概括整个系列:
Android 组件化解决的是 Android 内部解耦。
KMP 解决的是多个平台之间的业务逻辑复用。
CMP 则在 KMP 基础上,进一步解决多个平台之间的 UI 复用。
而真实项目的最终建议是:
KMP Core
优先建设
CMP
按页面价值选择
平台能力
不要强行共享
最后用一句话完成整个系列:
从 Android 组件化走向 KMP,并不是抛弃原来的 Android 能力,而是把公共业务继续下沉,把平台能力重新归位,再决定哪些 UI 值得共享。
更多推荐




所有评论(0)