从 Android 组件化到 KMP 跨平台底座(一):为什么 Android 组件化之后,还要学习 KMP?
前言
很多 Android 开发者第一次接触 KMP 时,都会产生一个疑问:
我已经做了 Android 组件化,网络库、存储库、扫码库、地图库也都封装好了,为什么还要学习 KMP?
甚至还会进一步怀疑:
如果以后使用 KMP,那我以前封装的 Android Library,是不是都没用了?
这个问题其实非常关键。
因为 Android 组件化和 KMP 看起来都在讲“模块拆分”“代码复用”“接口抽象”,但它们解决的并不是同一个层面的问题。
简单来说:
-
Android 组件化解决的是 Android 项目内部的解耦与复用
-
KMP 解决的是 Android、iOS、Web 等多个平台之间的业务逻辑复用
所以,学习 KMP 并不是推翻原来的 Android 组件化,而是把原来的组件化能力向跨平台方向继续扩展。
原来的 Android Library 也没有废掉,只是需要重新定位。
一、Android 组件化解决了什么问题?
先来看一个典型的 Android 组件化项目。
androidApp/
├── app
├── lib-network
├── lib-storage
├── lib-qrcode
├── lib-map
├── lib-log
├── feature-login
├── feature-device
└── feature-inspection
在没有组件化之前,项目里的代码可能全部放在 app 模块中:
-
网络请求写在页面里
-
数据库存储写在业务模块里
-
扫码逻辑和页面代码混在一起
-
地图 SDK 到处调用
-
登录状态被多个页面直接读取
-
不同业务模块互相依赖
随着项目越来越大,就会出现很多问题:
-
模块之间耦合严重
-
一个小改动影响多个页面
-
公共代码重复
-
编译速度变慢
-
不方便多人协作
-
其他项目无法复用已有能力
于是,我们开始做 Android 组件化。
例如:
lib-network
负责:
-
Retrofit、OkHttp 配置
-
请求拦截器
-
Token 注入
-
统一响应处理
-
网络异常转换
lib-storage
负责:
-
SharedPreferences
-
DataStore
-
Room
-
文件存储
-
缓存管理
lib-qrcode
负责:
-
CameraX 相机预览
-
ML Kit 二维码识别
-
权限申请
-
扫码页面
lib-map
负责:
-
高德地图初始化
-
定位
-
Marker 展示
-
地图生命周期管理
feature-device
负责:
-
设备列表
-
设备详情
-
设备状态管理
-
设备业务流程
这样一来,Android 项目内部的职责就清晰了。
不同模块可以独立开发、独立维护,也可以在多个 Android 项目中复用。
这就是 Android 组件化的价值。
但是,这里有一个非常重要的前提:
这些模块最终仍然运行在 Android 平台上。
即使我们把一个 Android 项目拆成了二十个模块,它们本质上仍然是 Android 模块。
二、Android 组件化解决不了什么问题?
假设现在公司要开发一个 iOS App。
Android 项目中已经有一套成熟的业务逻辑:
-
登录参数校验
-
Token 刷新规则
-
设备状态判断
-
二维码内容解析
-
地图点位数据模型
-
网络错误转换
-
巡检表单校验
-
设备列表分页逻辑
那么,这些代码能直接给 iOS 使用吗?
通常不能。
例如 Android 中可能有这样的二维码解析代码:
data class ScanResult(
val rawContent: String,
val deviceId: String?,
val type: String?
)
class ScanParser {
fun parse(rawContent: String): ScanResult {
val params = rawContent
.substringAfter("?", rawContent)
.split("&")
.mapNotNull {
val pair = it.split("=")
if (pair.size == 2) {
pair[0] to pair[1]
} else {
null
}
}
.toMap()
return ScanResult(
rawContent = rawContent,
deviceId = params["deviceId"],
type = params["type"]
)
}
}
这段代码本身并没有使用:
-
Context -
Activity -
CameraX -
ML Kit -
Android 权限 API
-
Android UI
它只是完成二维码字符串解析。
但如果它被放在一个普通 Android Library 中,iOS 依然无法直接使用。
于是,iOS 开发者很可能会使用 Swift 再写一遍相同逻辑。
Web 端也会使用 JavaScript 或 TypeScript 再写一遍。
最后,一个二维码解析规则可能出现三份实现:
Android:Kotlin 实现一份
iOS:Swift 实现一份
Web:TypeScript 实现一份
这时就会出现新的问题:
-
三端实现规则可能不一致
-
修改协议时需要同步修改三份代码
-
某个平台可能遗漏某个特殊情况
-
测试用例需要重复编写
-
业务问题难以统一排查
Android 组件化可以解决 Android 项目内部的问题,却无法解决多个平台之间的重复实现问题。
这就是 KMP 出现的意义。
三、KMP 解决的是什么问题?
KMP 的全称是 Kotlin Multiplatform。
它的核心目标不是让所有平台都运行 Android 代码,而是让多个平台共享一部分 Kotlin 代码。
例如:
Android
iOS
Web
Desktop
这些平台可以共享:
-
数据模型
-
参数校验
-
业务规则
-
网络请求
-
数据转换
-
错误处理
-
Repository
-
UseCase
-
状态管理
-
二维码结果解析
-
地图点位模型
平台差异明显的能力,则继续由各个平台分别实现。
例如:
-
Android 使用 CameraX
-
iOS 使用 AVFoundation
-
Web 使用浏览器扫码能力
虽然底层扫码方式不同,但扫码完成后得到的字符串,可以交给同一套公共代码解析。
整体流程可以理解为:
Android CameraX / ML Kit
│
▼
二维码原始字符串
│
▼
commonMain 中的 ScanParser
│
▼
统一的 ScanResult
iOS 也是一样:
iOS AVFoundation
│
▼
二维码原始字符串
│
▼
commonMain 中的 ScanParser
│
▼
统一的 ScanResult
Web 端同样可以复用这套解析逻辑。
KMP 真正解决的是:
不同平台虽然使用不同的系统能力,但可以共享相同的业务规则。
四、Android 组件化和 KMP 的区别
可以通过一张表理解两者的区别。
| 对比项 | Android 组件化 | KMP |
|---|---|---|
| 主要目标 | Android 项目内部解耦 | 多平台业务逻辑复用 |
| 运行平台 | Android | Android、iOS、Web、Desktop 等 |
| 典型模块 | Android Library | Kotlin Multiplatform Module |
| 是否可以使用 Android SDK | 可以 | commonMain 不可以 |
| 是否可以使用 Context | 可以 | commonMain 不可以 |
| 是否可以共享给 iOS | 不可以 | 可以 |
| 是否可以共享业务模型 | Android 内部可以 | 多个平台可以 |
| 是否替代平台开发 | 不会 | 不会 |
因此,这两者并不是竞争关系。
更准确地说:
Android 组件化是单平台架构能力,KMP 是多平台架构能力。
Android 组件化关注的是:
一个 Android App 应该如何拆分?
KMP 关注的是:
Android、iOS、Web 之间,哪些代码可以只写一份?
五、原来的 Android Library 是不是废了?
答案非常明确:
没有废,只是需要重新定位。
假设原来的 Android 项目中有以下模块:
lib-network
lib-qrcode
lib-map
lib-storage
feature-device
在进入 KMP 后,这些模块并不是全部删除,而是需要分析每个模块内部到底包含什么代码。
六、lib-qrcode 应该怎么重新定位?
原来的 lib-qrcode 可能同时包含了以下内容:
lib-qrcode
├── CameraX 相机预览
├── ML Kit 二维码识别
├── Android 权限申请
├── 扫码 Activity
├── 二维码内容解析
└── ScanResult 数据模型
这里面其实混合了两类代码。
第一类是 Android 平台能力:
-
CameraX
-
ML Kit Android SDK
-
Activity
-
权限申请
-
Android 生命周期处理
这些代码不能进入 commonMain。
第二类是纯业务逻辑:
-
ScanResult -
二维码参数解析
-
设备编号提取
-
二维码类型判断
-
二维码合法性校验
这些代码可以进入 commonMain。
迁移后可以拆成:
sharedLogic/
└── commonMain/
└── qrcode/
├── ScanResult.kt
├── ScanParser.kt
└── ScanValidator.kt
Android 平台侧继续保留:
androidApp/
└── qrcode/
├── CameraXScanner.kt
├── MlKitQrAnalyzer.kt
├── QrScannerActivity.kt
└── AndroidQrPermissionManager.kt
iOS 平台侧则可以实现:
iosApp/
└── qrcode/
├── AvFoundationScanner.swift
└── IosQrPermissionManager.swift
所以原来的 Android lib-qrcode 并没有失去价值。
它只是从原来的“完整扫码模块”,重新定位成:
Android 平台扫码适配层。
而二维码结果模型和解析规则,则被进一步下沉到了 KMP 公共层。
七、lib-map 应该怎么重新定位?
原来的地图模块可能包含:
lib-map
├── 高德 MapView
├── 高德定位
├── Marker 创建
├── 地图权限
├── 地图生命周期
├── GeoPoint
└── MapMarkerModel
其中:
data class GeoPoint(
val latitude: Double,
val longitude: Double
)
data class MapMarkerModel(
val id: String,
val title: String,
val point: GeoPoint
)
这些只是普通 Kotlin 数据模型,不依赖 Android SDK。
因此,它们可以放入 commonMain。
但是以下代码不能进入 commonMain:
-
MapView -
AMap -
AMapLocationClient -
Android 定位权限
-
Android 地图生命周期
因为这些都属于 Android 平台实现。
迁移后可以变成:
sharedLogic/
└── commonMain/
└── map/
├── GeoPoint.kt
├── MapMarkerModel.kt
└── MapPointMapper.kt
Android 使用高德地图:
androidApp/
└── map/
└── AMapScreen.kt
iOS 可以使用:
-
MapKit
-
高德地图 iOS SDK
Web 可以使用:
-
高德地图 JavaScript API
-
Google Maps JavaScript API
-
其他 Web 地图方案
三个平台的地图 UI 和 SDK 不一样,但它们使用的点位数据模型可以保持一致。
八、lib-network 能不能迁移到 KMP?
网络模块需要拆开分析。
原来的 lib-network 中可能包含:
lib-network
├── OkHttp
├── Retrofit
├── Android 网络状态监听
├── TokenInterceptor
├── BaseResponse
├── ResultDo
├── ErrorMapper
└── Repository
其中:
-
Retrofit 依赖 JVM
-
OkHttp 的具体使用可能依赖 JVM 或 Android
-
Android 网络状态监听依赖 Android SDK
这些代码不能直接原封不动地搬进 commonMain。
但是以下内容通常可以共享:
data class BaseResponse<T>(
val code: Int,
val message: String,
val data: T?
)
sealed class ApiResult<out T> {
data class Success<T>(
val data: T
) : ApiResult<T>()
data class Failure(
val code: String,
val message: String
) : ApiResult<Nothing>()
}
还可以共享:
-
错误码转换规则
-
Token 刷新业务流程
-
Repository 接口
-
请求参数模型
-
响应数据模型
-
DTO 到 Domain Model 的转换
-
分页业务逻辑
如果使用 Ktor Client,还可以进一步共享网络请求实现。
因此,原来的网络模块也不是被完全抛弃,而是重新拆分为:
公共网络逻辑
+
Android 平台网络适配
九、lib-storage 应该怎么处理?
Android 项目中常见的本地存储方式包括:
-
SharedPreferences
-
DataStore
-
Room
-
Android 文件系统
这些 API 不能直接在 commonMain 中使用。
但业务层通常并不应该直接依赖 SharedPreferences。
例如,业务代码真正关心的可能只是:
interface TokenStorage {
suspend fun saveAccessToken(token: String)
suspend fun getAccessToken(): String?
suspend fun clearToken()
}
commonMain 只定义业务需要的存储能力。
Android 可以使用 DataStore 实现:
class AndroidTokenStorage(
private val dataStore: DataStore<Preferences>
) : TokenStorage {
override suspend fun saveAccessToken(token: String) {
// Android DataStore 实现
}
override suspend fun getAccessToken(): String? {
// Android DataStore 实现
return null
}
override suspend fun clearToken() {
// Android DataStore 实现
}
}
iOS 可以使用:
-
UserDefaults
-
Keychain
-
文件存储
来实现同一个接口。
因此,原来的 lib-storage 可以继续作为 Android 存储实现层使用。
KMP 公共层只关心抽象后的存储能力。
十、一个模块能不能进入 commonMain,应该怎么判断?
可以使用一个非常实用的判断标准:
这段代码离开 Android SDK 后,还能不能成立?
如果一段代码依赖下面这些内容,通常不能直接放入 commonMain:
-
Context -
Activity -
Fragment -
View -
Application -
Service -
BroadcastReceiver -
CameraX
-
高德地图 Android SDK
-
Android WebView
-
Android 权限 API
-
Android 日志 API
-
Android 文件路径
-
SharedPreferences
如果一段代码只是普通 Kotlin 逻辑,通常可以考虑放入 commonMain:
-
data class -
enum class -
业务状态
-
参数校验
-
字符串解析
-
数据计算
-
数据转换
-
Repository 接口
-
UseCase
-
错误码映射
-
接口响应封装
-
分页规则
-
表单校验
例如:
| 原 Android 代码 | 能否进入 commonMain | 处理方式 |
|---|---|---|
ResultDo |
可以 | 改造成跨平台 Result 封装 |
BaseResponse |
可以 | 放入公共网络模型 |
LoginValidator |
可以 | 直接迁移 |
ScanParser |
可以 | 直接迁移 |
GeoPoint |
可以 | 直接迁移 |
DeviceStatus |
可以 | 直接迁移 |
高德 MapView |
不可以 | 保留在 Android 平台 |
| CameraX 扫码 | 不可以 | 保留在 Android 平台 |
| ML Kit Android | 不可以 | 保留在 Android 平台 |
| SharedPreferences | 不直接可以 | 抽象存储接口 |
Android Log |
不直接可以 | 抽象 Logger |
| WebView | 不可以 | 各平台分别实现 |
这里还要注意:
有些代码表面上是 data class,但仍然可能依赖 Android。
例如:
@Parcelize
data class Device(
val id: String,
val name: String
) : Parcelable
它依赖了:
-
Parcelable -
Parcelize
因此不能直接原封不动地放进 commonMain。
可以将公共模型改成:
data class Device(
val id: String,
val name: String
)
Android 页面跳转时,再由 Android 层决定如何传递数据。
所以判断标准不是看它是不是 data class,而是看它是否依赖平台 API。
十一、从 Android 组件化到 KMP,不是推倒重来
很多开发者一听到跨平台,就会想到重新建项目、重新写架构、重新封装所有代码。
实际上,更合理的方式不是推倒重来,而是逐步下沉公共逻辑。
例如原来的 Android 项目是:
androidApp
├── lib-network
├── lib-qrcode
├── lib-map
├── lib-storage
└── feature-device
可以先新增一个 KMP 模块:
sharedLogic
然后逐步迁移。
第一步,迁移最安全的纯模型:
sharedLogic
└── commonMain
├── Device
├── DeviceStatus
├── GeoPoint
└── ScanResult
第二步,迁移纯业务工具:
sharedLogic
└── commonMain
├── ScanParser
├── LoginValidator
└── DeviceStatusMapper
第三步,迁移结果封装和错误处理:
sharedLogic
└── commonMain
├── ApiResult
├── AppError
└── ErrorMapper
第四步,再考虑 Repository、UseCase 和网络层。
原来的 Android App 继续运行,原来的 Android Library 也继续使用。
只是随着公共逻辑逐步下沉,Android 模块会越来越专注于:
-
Android UI
-
Android SDK
-
权限
-
生命周期
-
相机
-
地图
-
WebView
-
系统服务
-
平台能力接入
这个过程更像是架构升级,而不是项目重写。
十二、设备巡检 Demo 中应该怎么拆?
这个系列后续会统一围绕一个“设备巡检 KMP Demo”展开。
Demo 包含:
-
登录
-
设备列表
-
设备详情
-
扫码识别设备
-
地图点位
-
巡检表单
-
网络请求
-
错误处理
-
平台日志
-
Token 存储
在 Android 组件化阶段,可能是:
lib-network
lib-storage
lib-qrcode
lib-map
feature-auth
feature-device
feature-inspection
进入 KMP 后,可以逐步演变成:
sharedLogic/
├── commonMain/
│ ├── model/
│ ├── result/
│ ├── network/
│ ├── repository/
│ ├── usecase/
│ ├── qrcode/
│ ├── map/
│ └── feature/
│
├── androidMain/
│ └── Android 平台实现
│
└── iosMain/
└── iOS 平台实现
Android 工程继续保留:
androidApp/
├── CameraX
├── ML Kit
├── 高德地图
├── Android 权限
├── Android UI
└── Android 生命周期
iOS 工程负责:
iosApp/
├── AVFoundation
├── MapKit
├── iOS 权限
├── SwiftUI / UIKit
└── iOS 生命周期
公共业务流程则只维护一份:
sharedLogic/commonMain
这才是从 Android 组件化走向 KMP 的真正意义。
十三、Android 组件化经验,反而是学习 KMP 的优势
有 Android 组件化经验的人,学习 KMP 通常会更容易。
因为你已经理解了很多重要思想:
-
模块职责应该单一
-
业务代码不应该直接依赖具体实现
-
接口和实现需要分离
-
公共能力应该下沉
-
业务模块之间应该控制依赖方向
-
强耦合 SDK 应该被封装
-
上层应该依赖抽象,而不是依赖具体平台能力
这些思想在 KMP 中仍然成立。
区别只是:
以前我们考虑的是:
Android 模块之间如何解耦?
现在还要进一步考虑:
公共业务层和 Android、iOS、Web 平台之间如何解耦?
因此,Android 组件化并不是 KMP 的对立面。
恰恰相反:
Android 组件化是学习 KMP 架构设计的重要基础。
如果之前完全没有模块化和接口抽象经验,进入 KMP 后反而更容易把所有代码都堆进 shared 模块,最终形成一个新的“大泥球”。
十四、不要追求所有代码都跨平台
学习 KMP 后,还有一种常见误区:
既然是跨平台,是不是共享代码越多越好?
答案是否定的。
KMP 的目标不是追求百分之百共享,而是共享真正适合共享的部分。
像下面这些强平台能力,本来就应该保留在平台侧:
-
地图
-
相机
-
扫码
-
蓝牙
-
NFC
-
WebView
-
推送
-
后台任务
-
权限
-
系统服务
-
平台生命周期
Android 使用 Android 最成熟的方案。
iOS 使用 iOS 最成熟的方案。
Web 使用 Web 最合适的方案。
KMP 公共层负责统一:
-
输入数据
-
输出数据
-
业务规则
-
状态定义
-
错误模型
-
数据解析
-
业务流程
这种设计通常比强行把所有平台能力都塞进公共层更加稳定。
十五、总结
Android 组件化和 KMP 解决的是两个不同层面的问题。
Android 组件化解决:
Android 项目内部如何解耦、拆分和复用。
KMP 解决:
Android、iOS、Web 等平台之间如何共享业务逻辑。
学习 KMP 并不意味着原来的 Android 组件化失去价值。
原来的 Android Library 会发生两种变化。
第一种,纯 Kotlin 业务代码继续下沉到 commonMain:
-
数据模型
-
结果封装
-
参数校验
-
二维码解析
-
地图点位模型
-
Repository
-
UseCase
-
错误处理
第二种,依赖 Android SDK 的模块继续保留,并重新定位为 Android 平台适配层:
-
CameraX
-
ML Kit
-
高德地图
-
WebView
-
权限
-
Context
-
Activity
-
Android 存储
-
Android 日志
所以,从 Android 组件化走向 KMP,并不是推倒重来,而是把原来的架构继续向前推进了一步:
Android 组件化
│
▼
提取纯 Kotlin 业务逻辑
│
▼
下沉到 commonMain
│
▼
Android Library 转为平台适配层
│
▼
形成可供 Android、iOS、Web 复用的 KMP 跨平台底座
最终可以用一句话概括:
Android 组件化解决单端解耦,KMP 解决多端复用;原来的 Android Library 没有废,而是重新定位成了 Android 平台适配层。
下一篇预告
下一篇我们先不急着进入 commonMain、androidMain 和 iosMain 的项目结构,而是先解决另一个 Android 开发者很容易产生的疑问:
《Flutter 也是跨平台,为什么还要学习 KMP + CMP?》
我已经学习过 Flutter,也已经知道 Flutter 可以同时开发 Android、iOS 和 Web。
那么:
-
Flutter 和 KMP 到底有什么区别?
-
Flutter 已经可以跨平台了,为什么还需要 KMP?
-
Flutter 是完整跨平台框架,KMP 为什么更强调业务逻辑共享?
-
CMP 和 Flutter 都可以共享 UI,它们之间又有什么区别?
-
已有 Android 原生项目,更适合接入 Flutter,还是逐步接入 KMP?
-
Flutter、KMP、CMP 是互相替代,还是可以同时存在?
下一篇将站在 Android 开发者和实际项目落地的角度,讲清 Flutter、KMP 与 CMP 的定位差异。
先明确为什么要选择 KMP,再正式进入 KMP 的项目结构和 Source Set。
更多推荐




所有评论(0)