前言

很多 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 平台适配层。


下一篇预告

下一篇我们先不急着进入 commonMainandroidMain 和 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。

Logo

一站式 AI 云服务平台

更多推荐