前言

前面八篇,我们已经完成了从 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 值得共享。

Logo

一站式 AI 云服务平台

更多推荐