前言很多 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负责SharedPreferencesDataStoreRoom文件存储缓存管理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] ) } }这段代码本身并没有使用ContextActivityCameraXML KitAndroid 权限 APIAndroid UI它只是完成二维码字符串解析。但如果它被放在一个普通 Android Library 中iOS 依然无法直接使用。于是iOS 开发者很可能会使用 Swift 再写一遍相同逻辑。Web 端也会使用 JavaScript 或 TypeScript 再写一遍。最后一个二维码解析规则可能出现三份实现AndroidKotlin 实现一份 iOSSwift 实现一份 WebTypeScript 实现一份这时就会出现新的问题三端实现规则可能不一致修改协议时需要同步修改三份代码某个平台可能遗漏某个特殊情况测试用例需要重复编写业务问题难以统一排查Android 组件化可以解决 Android 项目内部的问题却无法解决多个平台之间的重复实现问题。这就是 KMP 出现的意义。三、KMP 解决的是什么问题KMP 的全称是 Kotlin Multiplatform。它的核心目标不是让所有平台都运行 Android 代码而是让多个平台共享一部分 Kotlin 代码。例如Android iOS Web Desktop这些平台可以共享数据模型参数校验业务规则网络请求数据转换错误处理RepositoryUseCase状态管理二维码结果解析地图点位模型平台差异明显的能力则继续由各个平台分别实现。例如Android 使用 CameraXiOS 使用 AVFoundationWeb 使用浏览器扫码能力虽然底层扫码方式不同但扫码完成后得到的字符串可以交给同一套公共代码解析。整体流程可以理解为Android CameraX / ML Kit │ ▼ 二维码原始字符串 │ ▼ commonMain 中的 ScanParser │ ▼ 统一的 ScanResultiOS 也是一样iOS AVFoundation │ ▼ 二维码原始字符串 │ ▼ commonMain 中的 ScanParser │ ▼ 统一的 ScanResultWeb 端同样可以复用这套解析逻辑。KMP 真正解决的是不同平台虽然使用不同的系统能力但可以共享相同的业务规则。四、Android 组件化和 KMP 的区别可以通过一张表理解两者的区别。对比项Android 组件化KMP主要目标Android 项目内部解耦多平台业务逻辑复用运行平台AndroidAndroid、iOS、Web、Desktop 等典型模块Android LibraryKotlin 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 平台能力CameraXML Kit Android SDKActivity权限申请Android 生命周期处理这些代码不能进入commonMain。第二类是纯业务逻辑ScanResult二维码参数解析设备编号提取二维码类型判断二维码合法性校验这些代码可以进入commonMain。迁移后可以拆成sharedLogic/ └── commonMain/ └── qrcode/ ├── ScanResult.kt ├── ScanParser.kt └── ScanValidator.ktAndroid 平台侧继续保留androidApp/ └── qrcode/ ├── CameraXScanner.kt ├── MlKitQrAnalyzer.kt ├── QrScannerActivity.kt └── AndroidQrPermissionManager.ktiOS 平台侧则可以实现iosApp/ └── qrcode/ ├── AvFoundationScanner.swift └── IosQrPermissionManager.swift所以原来的 Androidlib-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。但是以下代码不能进入commonMainMapViewAMapAMapLocationClientAndroid 定位权限Android 地图生命周期因为这些都属于 Android 平台实现。迁移后可以变成sharedLogic/ └── commonMain/ └── map/ ├── GeoPoint.kt ├── MapMarkerModel.kt └── MapPointMapper.ktAndroid 使用高德地图androidApp/ └── map/ └── AMapScreen.ktiOS 可以使用MapKit高德地图 iOS SDKWeb 可以使用高德地图 JavaScript APIGoogle Maps JavaScript API其他 Web 地图方案三个平台的地图 UI 和 SDK 不一样但它们使用的点位数据模型可以保持一致。八、lib-network 能不能迁移到 KMP网络模块需要拆开分析。原来的lib-network中可能包含lib-network ├── OkHttp ├── Retrofit ├── Android 网络状态监听 ├── TokenInterceptor ├── BaseResponse ├── ResultDo ├── ErrorMapper └── Repository其中Retrofit 依赖 JVMOkHttp 的具体使用可能依赖 JVM 或 AndroidAndroid 网络状态监听依赖 Android SDK这些代码不能直接原封不动地搬进commonMain。但是以下内容通常可以共享data class BaseResponseT( val code: Int, val message: String, val data: T? )sealed class ApiResultout T { data class SuccessT( val data: T ) : ApiResultT() data class Failure( val code: String, val message: String ) : ApiResultNothing() }还可以共享错误码转换规则Token 刷新业务流程Repository 接口请求参数模型响应数据模型DTO 到 Domain Model 的转换分页业务逻辑如果使用 Ktor Client还可以进一步共享网络请求实现。因此原来的网络模块也不是被完全抛弃而是重新拆分为公共网络逻辑 Android 平台网络适配九、lib-storage 应该怎么处理Android 项目中常见的本地存储方式包括SharedPreferencesDataStoreRoomAndroid 文件系统这些 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: DataStorePreferences ) : 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 可以使用UserDefaultsKeychain文件存储来实现同一个接口。因此原来的lib-storage可以继续作为 Android 存储实现层使用。KMP 公共层只关心抽象后的存储能力。十、一个模块能不能进入 commonMain应该怎么判断可以使用一个非常实用的判断标准这段代码离开 Android SDK 后还能不能成立如果一段代码依赖下面这些内容通常不能直接放入commonMainContextActivityFragmentViewApplicationServiceBroadcastReceiverCameraX高德地图 Android SDKAndroid WebViewAndroid 权限 APIAndroid 日志 APIAndroid 文件路径SharedPreferences如果一段代码只是普通 Kotlin 逻辑通常可以考虑放入commonMaindata classenum class业务状态参数校验字符串解析数据计算数据转换Repository 接口UseCase错误码映射接口响应封装分页规则表单校验例如原 Android 代码能否进入 commonMain处理方式ResultDo可以改造成跨平台 Result 封装BaseResponse可以放入公共网络模型LoginValidator可以直接迁移ScanParser可以直接迁移GeoPoint可以直接迁移DeviceStatus可以直接迁移高德MapView不可以保留在 Android 平台CameraX 扫码不可以保留在 Android 平台ML Kit Android不可以保留在 Android 平台SharedPreferences不直接可以抽象存储接口AndroidLog不直接可以抽象 LoggerWebView不可以各平台分别实现这里还要注意有些代码表面上是data class但仍然可能依赖 Android。例如Parcelize data class Device( val id: String, val name: String ) : Parcelable它依赖了ParcelableParcelize因此不能直接原封不动地放进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 UIAndroid 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 的目标不是追求百分之百共享而是共享真正适合共享的部分。像下面这些强平台能力本来就应该保留在平台侧地图相机扫码蓝牙NFCWebView推送后台任务权限系统服务平台生命周期Android 使用 Android 最成熟的方案。iOS 使用 iOS 最成熟的方案。Web 使用 Web 最合适的方案。KMP 公共层负责统一输入数据输出数据业务规则状态定义错误模型数据解析业务流程这种设计通常比强行把所有平台能力都塞进公共层更加稳定。十五、总结Android 组件化和 KMP 解决的是两个不同层面的问题。Android 组件化解决Android 项目内部如何解耦、拆分和复用。KMP 解决Android、iOS、Web 等平台之间如何共享业务逻辑。学习 KMP 并不意味着原来的 Android 组件化失去价值。原来的 Android Library 会发生两种变化。第一种纯 Kotlin 业务代码继续下沉到commonMain数据模型结果封装参数校验二维码解析地图点位模型RepositoryUseCase错误处理第二种依赖 Android SDK 的模块继续保留并重新定位为 Android 平台适配层CameraXML Kit高德地图WebView权限ContextActivityAndroid 存储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 已经可以跨平台了为什么还需要 KMPFlutter 是完整跨平台框架KMP 为什么更强调业务逻辑共享CMP 和 Flutter 都可以共享 UI它们之间又有什么区别已有 Android 原生项目更适合接入 Flutter还是逐步接入 KMPFlutter、KMP、CMP 是互相替代还是可以同时存在下一篇将站在 Android 开发者和实际项目落地的角度讲清 Flutter、KMP 与 CMP 的定位差异。先明确为什么要选择 KMP再正式进入 KMP 的项目结构和 Source Set。