:KMP 与 CMP 的关系——到底要不要共享 UI?)
前言前面八篇我们已经完成了从 Android 组件化到 KMP 跨平台业务底座的完整推演Android 组件化 │ ▼ 识别纯 Kotlin 业务逻辑 │ ▼ 迁入 commonMain │ ▼ 建立 androidMain / iosMain 平台适配 │ ▼ sharedLogic 分层 │ ▼ KMP 多模块组件化走到这里Android、iOS 甚至 Web 已经可以共享数据模型网络请求RepositoryUseCase错误处理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 后是否必须使用 CMPAndroid Compose 页面能不能直接迁到 CMP哪些页面适合共享哪些页面不应该强行共享sharedLogic和sharedUI应该怎么划分扫码、地图、相机如何接入共享 UICMP 与 SwiftUI、UIKit、Android View 能不能混合KMP CMP 与 KMP Flutter 应该怎么选择设备巡检项目最终应该采用哪种路线一、先说结论KMP 不等于 CMPKMP 的全称是Kotlin Multiplatform它解决的是Kotlin 代码如何在 Android、iOS、Web、Desktop 等多个平台之间共享。CMP 的全称是Compose Multiplatform它解决的是Compose UI 如何在多个平台之间共享。因此两者的关系是KMP 是跨平台基础能力 CMP 是建立在 KMP 之上的跨平台 UI 框架可以用一个简单公式表示KMP 跨平台代码共享能力 KMP CMP 跨平台业务逻辑 跨平台 Compose UICompose 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 └── DeviceListStateAndroid 使用 ComposeComposable fun AndroidDeviceListScreen( state: DeviceListState, onDeviceClick: (String) - Unit ) { // Android Compose 页面 }iOS 使用 SwiftUIstruct DeviceListView: View { let state: DeviceListState var body: some View { // SwiftUI 页面 } }两端 UI 分别实现但共用DeviceDeviceListStateRepositoryUseCase网络请求错误处理这种路线的优点是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因为业务层不应该知道页面最终使用ComposeSwiftUIFlutterVueReact四、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.currentToast.makeText(...)Intent(...)ActivityResultLauncherandroidx.compose.ui.res.painterResourceandroid.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(提交) } }AndroidDeviceDetailScreen( device device, onSubmit { Toast.makeText( context, 操作成功, Toast.LENGTH_SHORT ).show() } )iOSDeviceDetailScreen │ ▼ iOS 平台显示提示共享 UI 不应该直接控制平台 Toast、Intent 或 ViewController。第三类不建议第一阶段强行迁移的页面例如CameraX 相机页面扫码预览页高德地图页WebView 容器视频播放器蓝牙设备控制页系统文件选择器强依赖 Activity 的页面强依赖 UIKit Controller 的页面复杂系统手势和平台导航页面这些页面并不是永远不能和 CMP 结合。但它们通常需要原生 View 嵌入权限管理生命周期处理平台 SDK 适配事件桥接因此第一阶段更稳妥的方式是普通页面共享 强平台页面继续平台化六、哪些页面最适合使用 CMP可以把页面分成三档。第一档优先共享典型页面登录页 注册页 设备列表 设备详情 巡检记录列表 普通巡检表单 个人中心 设置页 加载页 空状态 错误页 通用弹窗这些页面通常具有数据驱动平台差异少交互逻辑统一UI 结构接近不依赖强平台 SDK非常适合进入sharedUI/commonMain第二档共享外壳平台实现核心区域例如扫码页 地图页 图片选择页 视频播放页 WebView 页面可以共享顶部标题页面背景提示文案底部操作栏加载状态错误状态操作按钮平台侧实现相机预览MapViewWebView视频播放器系统文件选择器整体结构共享 Compose 页面外壳 ├── 共享标题 ├── 共享提示 ├── 平台原生内容区域 └── 共享底部操作Compose Multiplatform 支持将 Compose 嵌入 UIKit 或 SwiftUI也支持在 Compose 页面中嵌入 UIKit、SwiftUI 等平台内容。第三档完全平台化例如Android 前台服务控制页 iOS 系统专属页面 复杂高德地图页面 深度相机页面 系统设置页面 平台支付页面 平台级导航和系统 TabBar如果平台差异明显直接分别实现通常更加稳定。共享 UI 不应该成为必须完成的 KPI。七、共享 UI 不是共享全部平台行为例如设备详情页可以共享设备名称 设备状态 基本信息 操作按钮 加载状态但点击“拨打电话”后Android 可能调用Intent.ACTION_DIALiOS 可能调用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 AppErrorsharedUI提供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: ListDeviceUiModel 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 │ ▼ sharedLogicFlutter 也可以通过桥接使用相同 Presentation 状态。但这种结构会增加 Module 数量和复杂度。对于当前设备巡检 Demo可以先采用sharedLogic sharedUI等确定需要“共享状态但不共享 UI”时再拆sharedPresentation。十一、共享 ViewModel 还是只共享 UseCase主要有三种方案。方案一只共享 UseCasesharedLogic 提供 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 页面行为一致单一团队维护成本较低缺点平台差异处理更复杂页面会更加依赖 CMPiOS 团队的 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 └── InspectionRouteAndroid、iOS 都使用同一套 Compose 导航。优点页面流程只维护一份路由参数统一页面转场逻辑共享适合全 CMP AppCompose Multiplatform 当前已经提供适用于跨平台项目的 Compose Navigation 方案可以在 Android、iOS、Desktop 和 Web 等目标上组织共享导航。路线二页面共享导航平台化结构sharedUI 负责单个 Screen 内容 Android 使用自己的导航宿主 iOS 使用 SwiftUI NavigationStack共享页面通过回调发出导航事件Composable fun DeviceListScreen( state: DeviceListUiState, onOpenDevice: (String) - Unit )AndroidonOpenDevice → Android NavigationiOSonOpenDevice → 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 / 高德地图 iOSKMP 共享GeoPoint MapMarkerModel GetDeviceMarkersUseCase这是最稳妥的方案。路线二CMP 页面嵌入原生 MapView共享页面标题筛选条件设备卡片加载状态错误状态平台提供高德 MapViewMapKit View这是共享率和平台能力之间的折中方案。路线三使用 CMP 地图库如果项目选用的地图库已经提供成熟 KMP/CMP 支持也可以进一步共享。但需要评估平台功能完整度地图厂商要求中国坐标系定位能力Marker 聚合路线规划生命周期商业授权不要只因为“可以共享”就替换已经稳定运行的地图 SDK。十八、CMP 和 Flutter 有什么关系CMP 和 Flutter 都可以负责跨平台 UI。区别主要在技术体系。KMP CMP Kotlin 业务底座 Compose UIKMP Flutter Kotlin 业务底座 Flutter UI对比对比项KMP CMPKMP FlutterUI 语言KotlinDartUI 框架ComposeFlutter Widget业务底座KMPKMPAndroid Compose 技能复用高低KMP 调用链直接需要 Flutter 桥接模型转换较少Dart 与 Kotlin 转换状态桥接Kotlin 内部Flow、Future、Stream 适配技术栈数量相对少Kotlin Dart 平台桥接适合团队Kotlin/Compose 团队Flutter UI 团队如果已经有成熟 Flutter UI 团队KMP Flutter 完全可行。但如果团队核心是 Android Kotlin 和 ComposeKMP CMP 通常更自然。十九、CMP 的优势是什么1. Android Compose 能力可以继续复用团队已有的Compose 布局经验State 思维Material 组件动画ModifierCoroutineFlowKotlin都可以继续使用。2. 减少 UI 重复开发Android 和 iOS 不需要分别实现登录页列表页表单页通用组件加载页面错误页面3. 页面行为更容易保持一致例如表单校验错误的位置、加载逻辑、按钮状态和空页面都可以只维护一套。4. 与 KMP 业务底座连接自然调用链可以全部使用 KotlinComposable │ ▼ State Holder │ ▼ UseCase │ ▼ Repository不需要 Dart、Swift 和 Kotlin 之间大量模型转换。5. 支持渐进式接入CMP 可以从一个组件 一个页面 一个 Feature逐步接入而不是要求一次替换整个 App。官方也将渐进式采用和原生 UI 互操作作为 Compose Multiplatform 的主要能力之一。二十、CMP 的代价是什么1. 仍然需要理解 iOS使用 CMP 不代表可以完全忽略XcodeSwiftUIKitSwiftUIInfo.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推荐平台化扫码页面 地图页面 权限处理 WebViewAndroidCameraX ML Kit 高德地图 Android 权限iOSAVFoundation 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先确保RepositoryUseCase网络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 CMPAndroid 项目已经全面 Compose 化iOS UI 尚未完整建设团队主要是 Kotlin 开发者普通列表和表单页面较多Android 与 iOS 产品结构一致希望减少双端 UI 重复能接受部分平台页面单独实现结构sharedLogic sharedUI 平台能力适配二十八、什么时候应该选择 KMP Flutter如果项目符合以下特点可以考虑 KMP Flutter团队已有成熟 Flutter UI 能力已经存在大量 Flutter 页面希望 KMP 作为独立业务 SDKKMP 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 值得共享。