从 Android 组件化到 KMP 跨平台底座(八):KMP 多模块组件化——core-model、core-network、feature-device 的依赖怎么拆?
前言
上一篇我们已经在单个 sharedLogic Module 中建立了基本分层:
sharedLogic/
├── core/
│ ├── result/
│ ├── error/
│ ├── network/
│ ├── storage/
│ └── platform/
│
└── feature/
├── auth/
├── device/
└── inspection/
这种结构适合:
-
KMP 项目刚开始搭建
-
业务模块数量不多
-
正在从 Android 项目渐进式迁移
-
需要先跑通 Android 和 iOS 链路
-
团队规模较小
但随着代码不断增加,单个 sharedLogic 会逐渐出现新的问题:
所有 Feature 都能随意引用
Domain 可以直接调用 Data
设备业务可以直接访问登录模块内部实现
修改网络层会重新编译整个 sharedLogic
其他项目只能整体引入 sharedLogic
无法通过 Gradle 真正限制依赖方向
这时,仅靠 package 目录已经无法完全控制架构边界。
我们需要进一步把:
逻辑边界
升级为:
Gradle Module 物理边界
最终形成:
core-model
core-result
core-network
core-storage
core-platform
feature-auth
feature-device
feature-inspection
sharedLogic
sharedUI
androidApp
iosApp
webApp
这就是 KMP 多模块组件化。
一、KMP 也可以做多模块组件化
很多 Android 开发者刚接触 KMP 时,会把整个跨平台业务都放进一个:
shared
或者:
sharedLogic
然后认为 KMP 项目只能有一个公共模块。
实际上,KMP 项目同样可以包含多个 Gradle Module。
例如:
core-model/
core-result/
core-network/
feature-auth/
feature-device/
feature-inspection/
每个 Module 都可以是一个独立的 Kotlin Multiplatform Library。
每个 KMP Module 都可以拥有自己的:
commonMain
androidMain
iosMain
jsMain
wasmJsMain
例如:
feature-device/
└── src/
├── commonMain/
├── androidMain/
├── iosMain/
└── commonTest/
这意味着,一个 Feature 内部仍然可以同时存在:
公共设备业务
Android 设备实现
iOS 设备实现
Web 设备实现
KMP 的 Source Set 属于具体 Module,一个项目中可以存在多个拥有独立 Target 和 Source Set 的 KMP Module。公共 Source Set 负责共享代码,平台 Source Set 负责对应 Target 的实现。
所以:
Android 组件化
=
多个 Android Library
KMP 组件化
=
多个 Kotlin Multiplatform Library
+
平台专属 Module
+
平台 App
二、KMP Module 和 Android Library 有什么区别?
普通 Android Library 通常是:
lib-network/
└── src/
├── main/
├── test/
└── androidTest/
它最终只服务 Android。
KMP Library 则可能是:
core-network/
└── src/
├── commonMain/
├── androidMain/
├── iosMain/
├── commonTest/
├── androidUnitTest/
└── iosTest/
它可以同时面向多个平台。
可以简单理解为:
| 对比项 | Android Library | KMP Library |
|---|---|---|
| 主要目标 | Android | Android、iOS、Web、Desktop 等 |
| 公共源码 | src/main |
commonMain |
| Android 源码 | src/main |
androidMain |
| iOS 源码 | 无 | iosMain |
| 是否可以被 iOS 复用 | 不可以 | 可以 |
| Gradle 输出 | AAR 等 | 平台变体、元数据、Framework 等 |
| 平台 SDK | 直接使用 Android SDK | 在对应平台 Source Set 使用 |
现在 Android 官方提供了专门面向 KMP Library 的 Android Gradle 插件:
com.android.kotlin.multiplatform.library
在使用该插件的项目中,Android Target 可以通过:
kotlin {
androidLibrary {
namespace = "com.sqx.core.model"
compileSdk = 36
minSdk = 24
}
}
进行配置。该插件用于在 KMP Library 中提供 Android Target,并改善构建配置和 Android Studio 集成。
部分旧工程仍然可能使用:
androidTarget()
配合传统 Android Library 插件。
两种写法的具体 Gradle 配置不同,但多模块依赖和 Source Set 边界的核心思想是一致的。
三、不是所有 Module 都需要支持所有平台
虽然每个 KMP Module 都可以声明多个 Target,但不代表所有 Module 都必须同时支持:
Android
iOS
Web
Desktop
应该根据模块职责决定。
例如:
core-model
Android:需要
iOS:需要
Web:需要
Desktop:可能需要
适合声明全部目标。
core-network
Android:需要
iOS:需要
Web:根据项目决定
如果 Web 端也要直接复用 KMP 网络层,可以声明 Web Target。
android-qrcode
Android:需要
iOS:不需要
它本身就是 Android 专属 Module,不必强行改成 KMP。
ios-scanner
iOS:需要
Android:不需要
可以继续放在 Xcode 工程,或者在 iosMain 中实现部分 Kotlin/Native 适配。
所以目标不是:
把所有 Module 都改成 KMP
而是:
真正需要跨平台复用的 Module
使用 KMP
只有 Android 使用的 Module
继续使用 Android Library
只有 iOS 使用的能力
继续保留在 iOS 平台
四、推荐先把 Module 分成五类
一个比较清晰的 KMP 多模块项目,可以把 Module 分成五类。
第一类:基础模型模块
例如:
core-model
core-result
它们位于依赖图最底层,尽量少依赖其他业务模块。
第二类:技术基础模块
例如:
core-network
core-storage
core-database
core-platform
core-logging
它们负责跨 Feature 复用的技术能力。
第三类:业务 Feature 模块
例如:
feature-auth
feature-device
feature-inspection
它们负责具体业务闭环。
第四类:平台适配模块
例如:
android-qrcode
android-map
android-webview
android-bluetooth
ios-scanner
ios-map
这些模块负责平台 SDK、权限、生命周期和硬件能力。
第五类:组装和应用入口
例如:
sharedLogic
sharedUI
androidApp
iosApp
webApp
它们负责组装各个业务模块并提供平台入口。
五、推荐的整体模块结构
设备巡检项目可以逐步演变成:
lift-kmp-core/
├── build.gradle.kts
├── settings.gradle.kts
│
├── core-model/
├── core-result/
├── core-network/
├── core-storage/
├── core-database/
├── core-platform/
├── core-logging/
│
├── core-qrcode/
├── core-map/
│
├── feature-auth/
├── feature-device/
├── feature-inspection/
│
├── sharedLogic/
├── sharedUI/
│
├── android-qrcode/
├── android-map/
├── android-webview/
├── android-bluetooth/
│
├── androidApp/
├── iosApp/
└── webApp/
对应依赖方向:
androidApp
│
┌─────────────┴─────────────┐
│ │
sharedUI Android 平台模块
│ android-qrcode / map
▼
sharedLogic
│
┌─────────┼───────────┐
│ │ │
feature-auth feature-device feature-inspection
│ │ │
└─────────┼───────────┘
│
┌─────────┼───────────┐
│ │ │
core-network core-storage core-platform
│ │ │
└─────────┼───────────┘
│
┌─────────┴───────────┐
│ │
core-model core-result
最重要的规则是:
上层依赖下层
下层不能反向依赖上层
例如:
feature-device
可以依赖 core-network
core-network
不能反向依赖 feature-device
六、settings.gradle.kts 中注册模块
首先需要在 settings.gradle.kts 中注册模块:
include(":core-model")
include(":core-result")
include(":core-network")
include(":core-storage")
include(":core-platform")
include(":core-qrcode")
include(":core-map")
include(":feature-auth")
include(":feature-device")
include(":feature-inspection")
include(":sharedLogic")
include(":sharedUI")
include(":android-qrcode")
include(":android-map")
include(":androidApp")
Gradle Module 名称不必和包名完全相同,但建议保持清晰对应。
例如:
Module:
:feature-device
包名:
com.sqx.liftkmp.feature.device
七、core-model 应该怎么设计?
core-model 位于依赖图底层。
一个简单配置如下:
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
}
kotlin {
androidLibrary {
namespace = "com.sqx.liftkmp.core.model"
compileSdk = libs.versions.android.compileSdk
.get()
.toInt()
minSdk = libs.versions.android.minSdk
.get()
.toInt()
}
iosArm64()
iosSimulatorArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
源码结构:
core-model/
└── src/commonMain/kotlin/
└── com/sqx/liftkmp/core/model/
├── GeoPoint.kt
├── PageData.kt
└── UserId.kt
例如:
data class GeoPoint(
val latitude: Double,
val longitude: Double
)
data class PageData<T>(
val items: List<T>,
val page: Int,
val pageSize: Int,
val total: Long
)
八、不要把所有业务模型都塞进 core-model
core-model 很容易变成新的大杂烩:
core-model/
├── User.kt
├── Device.kt
├── InspectionForm.kt
├── Order.kt
├── Message.kt
├── LoginResult.kt
└── ScanResult.kt
这样虽然解决了 Module 依赖问题,却破坏了业务边界。
更合理的是:
真正跨 Feature 的基础模型
放 core-model
具体业务模型
放对应 feature
例如:
core-model
├── GeoPoint
├── PageData
├── FileData
└── UserId
feature-device
├── Device
└── DeviceStatus
feature-inspection
├── InspectionForm
└── InspectionItem
如果 feature-inspection 只需要设备 ID,不一定要依赖完整的 Device:
@JvmInline
value class DeviceId(
val value: String
)
可以把稳定的 DeviceId 放入 core-model。
这样能避免 Feature 之间形成不必要的强依赖。
九、core-result 应该怎么设计?
core-result 可以定义统一结果和错误模型:
core-result/
└── src/commonMain/kotlin/
├── AppResult.kt
└── AppError.kt
sealed interface AppResult<out T> {
data class Success<T>(
val data: T
) : AppResult<T>
data class Failure(
val error: AppError
) : AppResult<Nothing>
}
sealed interface AppError {
data object Unauthorized : AppError
data object NetworkUnavailable : AppError
data object Timeout : AppError
data class Server(
val code: String,
val message: String
) : AppError
data class Validation(
val message: String
) : AppError
data class Unknown(
val message: String
) : AppError
}
如果 AppError 使用了 core-model 中的类型,就可以建立:
commonMain.dependencies {
implementation(project(":core-model"))
}
如果没有使用,则保持 core-result 独立即可。
基础 Module 越独立,越容易复用。
十、KMP Module 之间怎么建立依赖?
假设:
feature-device
需要使用:
core-model
core-result
core-network
可以在 feature-device/build.gradle.kts 中配置:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":core-model"))
implementation(project(":core-result"))
implementation(project(":core-network"))
}
}
}
这里的含义是:
feature-device/commonMain
依赖
其他 KMP Module 的公共代码
当一个 KMP Module 的 commonMain 依赖另一个 KMP Module 时,Kotlin Gradle 插件会为不同 Target 自动选择对应的平台变体;Android 编译会使用依赖模块的 Android 变体,iOS 编译会使用相应的 Native 变体。
也就是说,不需要分别写:
androidMain.dependencies {
implementation(project(":core-model"))
}
iosMain.dependencies {
implementation(project(":core-model"))
}
只要这个依赖是所有平台都要使用的,就应优先写在:
commonMain.dependencies
十一、依赖模块必须提供兼容的 Target
虽然 commonMain 只写一次依赖,但被依赖 Module 必须提供兼容的平台变体。
假设:
feature-device
支持 Android 和 iOS
它依赖:
core-network
那么 core-network 也应该提供 Android 和 iOS 对应 Target。
例如:
kotlin {
androidLibrary {
namespace = "com.sqx.liftkmp.core.network"
compileSdk = 36
minSdk = 24
}
iosArm64()
iosSimulatorArm64()
}
如果 core-network 只声明 Android,而 feature-device 还需要编译 iOS,那么 iOS 构建时就无法找到兼容变体。
因此,依赖关系不仅要看代码层,还要看 Target 是否兼容:
消费者支持 Android + iOS
依赖模块至少也需要提供
消费者实际使用到的平台变体
这也是为什么底层 core-* Module 通常应该具有较完整的平台支持。
十二、androidMain 如何依赖 Android 专属 Module?
假设已经有一个 Android 扫码模块:
android-qrcode
它是普通 Android Library,内部使用:
-
CameraX
-
ML Kit
-
Android 权限
-
LifecycleOwner
那么 KMP Module 的 commonMain 不能依赖它。
错误写法:
commonMain.dependencies {
implementation(project(":android-qrcode"))
}
因为 iOS 无法解析 Android Library。
正确方式是放在:
androidMain.dependencies {
implementation(project(":android-qrcode"))
}
例如:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":core-qrcode"))
}
androidMain.dependencies {
implementation(project(":android-qrcode"))
}
}
}
含义是:
commonMain
使用跨平台二维码模型和解析规则
androidMain
使用 Android CameraX 扫码实现
Android 专属依赖可以像普通 Android 项目一样添加到 KMP 的 Android Source Set 中,而不会进入 iOS 等其他平台。
十三、iosMain 如何依赖 iOS 平台能力?
iosMain 可以直接调用 Apple 平台 API,例如:
import platform.Foundation.NSLog
import platform.UIKit.UIDevice
如果 iOS 侧需要额外 Native 依赖,则应该配置在:
iosMain.dependencies
或者对应的 Apple Source Set 中。
例如:
kotlin {
sourceSets {
iosMain.dependencies {
implementation(
"com.squareup.sqldelight:native-driver:版本号"
)
}
}
}
平台专属依赖应放入对应的平台 Source Set,而不能进入 commonMain。
但是,对相机预览、MapKit 页面等强 UI 能力,仍然可以选择直接放在:
iosApp
而不是为了多模块形式,强行把所有 iOS UI SDK 都塞进 KMP Module。
十四、feature-device 应该怎么拆?
一个简单的 feature-device Module 可以同时包含 Domain 和 Data:
feature-device/
└── src/commonMain/kotlin/
└── feature/device/
├── domain/
│ ├── model/
│ │ ├── Device.kt
│ │ └── DeviceStatus.kt
│ │
│ ├── repository/
│ │ └── DeviceRepository.kt
│ │
│ └── usecase/
│ ├── GetDeviceListUseCase.kt
│ └── GetDeviceDetailUseCase.kt
│
└── data/
├── remote/
├── local/
├── mapper/
└── repository/
它的依赖可以是:
kotlin {
sourceSets {
commonMain.dependencies {
api(project(":core-model"))
api(project(":core-result"))
implementation(project(":core-network"))
implementation(project(":core-storage"))
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
这里故意同时使用了:
api
implementation
它们的区别后面单独讲。
十五、feature-auth 应该依赖哪些模块?
登录模块可能依赖:
core-model
core-result
core-network
core-storage
core-logging
配置:
kotlin {
sourceSets {
commonMain.dependencies {
api(project(":core-model"))
api(project(":core-result"))
implementation(project(":core-network"))
implementation(project(":core-storage"))
implementation(project(":core-logging"))
}
}
}
典型调用链:
LoginUseCase
│
├── AuthRepository
│ │
│ ▼
│ AuthRepositoryImpl
│ │
│ ▼
│ core-network
│
└── core-storage
feature-auth 不应该依赖:
feature-device
feature-inspection
sharedUI
androidApp
登录业务应该处于相对独立的位置。
十六、feature-inspection 应该依赖 feature-device 吗?
这是多模块架构中非常容易出现的问题。
巡检业务可能需要:
-
设备 ID
-
设备名称
-
设备详情
-
设备状态
最简单的方式是:
feature-inspection
直接依赖
feature-device
commonMain.dependencies {
implementation(project(":feature-device"))
}
技术上可以,但要考虑后果:
inspection
是否需要 device 的全部实现?
还是只需要少量稳定业务能力?
如果只需要 DeviceId,更适合依赖:
core-model
如果需要查询设备详情,可以有三种方案。
方案一:直接依赖 feature-device
适合:
-
项目规模较小
-
两个 Feature 本来就存在明确单向依赖
-
不会形成循环依赖
feature-inspection
│
▼
feature-device
方案二:拆分 feature-device-api 和 feature-device-impl
结构:
feature-device-api
├── Device
├── DeviceRepository
└── GetDeviceDetailUseCase
feature-device-impl
├── DeviceDto
├── DeviceRemoteDataSource
└── DeviceRepositoryImpl
依赖方向:
feature-inspection
│
▼
feature-device-api
▲
│
feature-device-impl
这样巡检模块只能看到设备业务契约,看不到设备数据层实现。
方案三:由上层协调两个 Feature
例如在 sharedLogic 中定义:
class StartInspectionCoordinator(
private val getDeviceDetailUseCase:
GetDeviceDetailUseCase,
private val createInspectionUseCase:
CreateInspectionUseCase
)
这时:
feature-device
不依赖 feature-inspection
feature-inspection
也不依赖 feature-device
sharedLogic
负责组合两个 Feature
对于业务边界比较清晰的项目,这种方式最干净。
十七、如何避免 Feature 循环依赖?
最危险的情况是:
feature-device
依赖 feature-inspection
feature-inspection
又依赖 feature-device
形成:
feature-device
▲ │
│ ▼
feature-inspection
Gradle 会直接出现循环依赖问题,架构上也说明两个 Feature 的边界没有划清。
解决方法通常有四种。
1. 下沉公共模型
将双方都需要的稳定类型下沉到:
core-model
例如:
DeviceId
InspectionId
GeoPoint
2. 提取公共业务契约
建立:
feature-device-api
或者:
device-contract
只放:
-
接口
-
稳定模型
-
业务事件
3. 通过上层协调器组合
把跨 Feature 流程放到:
sharedLogic
app-domain
feature-coordinator
而不是让 Feature 互相调用实现。
4. 重新判断是否真的是两个 Feature
如果两个模块始终同时修改、互相强依赖、无法独立存在,可能说明它们原本就属于同一个业务边界。
不要为了模块数量,强行拆开高度内聚的业务。
十八、api 和 implementation 有什么区别?
在多模块项目中,这两个关键字非常重要。
1. implementation
implementation(project(":core-network"))
表示:
当前 Module 内部使用这个依赖,但不希望把它作为自己的公共 API 暴露给上层。
例如:
feature-device
内部使用 Ktor 和 core-network
上层 UI
不应该直接感知网络实现
所以:
implementation(project(":core-network"))
更加合适。
2. api
api(project(":core-model"))
表示:
当前 Module 的公共 API 中暴露了这个依赖的类型,使用当前 Module 的上层也需要看到它。
例如:
interface DeviceRepository {
suspend fun getDeviceList():
AppResult<List<Device>>
}
这里公共签名暴露了:
AppResult
Device
如果它们分别来自:
core-result
core-model
那么 feature-device 可以使用:
api(project(":core-result"))
api(project(":core-model"))
这样使用 feature-device 的上层能够看到这些公共类型。
3. 判断原则
可以先使用一个简单判断:
依赖类型出现在当前 Module 的公共函数、
接口、属性、父类和泛型签名中
→ 考虑 api
依赖只在 Module 内部实现使用
→ implementation
例如:
core-model
出现在 DeviceRepository 返回值中
→ api
core-network
只在 DeviceRepositoryImpl 内部使用
→ implementation
不过也不要把所有依赖都改成 api。
api 会增加依赖暴露范围,使上层更容易绕过模块边界,也可能增加编译影响范围。
默认优先:
implementation
只有公共签名确实需要时再使用:
api
十九、api 对 iOS Framework 导出还有特殊意义
Android App 可以直接依赖多个 Gradle Module。
但 iOS App 通常需要通过 Kotlin/Native Framework 使用 KMP 代码。
当项目内部已经拆成:
core-model
core-result
feature-auth
feature-device
feature-inspection
通常不希望 Swift 分别集成多个 Kotlin Framework。
更常见的做法是创建一个:
sharedLogic
作为统一出口。
iosApp
│
▼
SharedLogic.framework
│
├── feature-auth
├── feature-device
├── feature-inspection
├── core-model
└── core-result
Kotlin/Native 支持通过一个上层 Framework 显式导出依赖模块,形成供 Swift 使用的统一 Framework。只有对应 Source Set 中的 api 依赖才能被 Framework 的 export() 导出。
二十、sharedLogic 聚合模块应该怎么设计?
sharedLogic 可以不再存放大量具体业务代码,而是作为:
业务组装模块
+
iOS Framework 出口
配置示例:
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
}
kotlin {
androidLibrary {
namespace = "com.sqx.liftkmp.shared.logic"
compileSdk = libs.versions.android.compileSdk
.get()
.toInt()
minSdk = libs.versions.android.minSdk
.get()
.toInt()
}
val iosTargets = listOf(
iosArm64(),
iosSimulatorArm64()
)
iosTargets.forEach { target ->
target.binaries.framework {
baseName = "SharedLogic"
isStatic = true
export(project(":core-model"))
export(project(":core-result"))
export(project(":feature-auth"))
export(project(":feature-device"))
export(project(":feature-inspection"))
}
}
sourceSets {
commonMain.dependencies {
api(project(":core-model"))
api(project(":core-result"))
api(project(":feature-auth"))
api(project(":feature-device"))
api(project(":feature-inspection"))
implementation(project(":core-network"))
implementation(project(":core-storage"))
implementation(project(":core-platform"))
}
}
}
这样:
Android
可以依赖 sharedLogic
iOS
可以集成 SharedLogic Framework
Swift
通过一个 Framework 使用多个内部 KMP Module
二十一、不要轻易使用 transitiveExport
Kotlin/Native Framework 支持:
transitiveExport = true
它会继续导出依赖模块的传递依赖。
看起来很方便,但可能导致:
-
Framework API 急剧膨胀
-
无关依赖暴露给 Swift
-
二进制体积增大
-
编译时间增加
-
模块边界变得模糊
官方文档也不建议在大多数情况下启用传递导出,而是建议显式导出 Swift 真正需要访问的模块。
因此更推荐:
export(project(":core-model"))
export(project(":core-result"))
export(project(":feature-device"))
而不是:
transitiveExport = true
二十二、Android App 应该依赖 sharedLogic 还是分别依赖 Feature?
有两种方式。
方式一:Android App 只依赖 sharedLogic
dependencies {
implementation(project(":sharedLogic"))
}
优点:
-
Android App 依赖简单
-
和 iOS 统一使用一个业务入口
-
Module 组装集中
-
平台 App 更像纯壳
缺点:
-
sharedLogic 可能暴露过多业务
-
Android 无法按需只引用某个 Feature
方式二:Android App 分别依赖 Feature
dependencies {
implementation(project(":feature-auth"))
implementation(project(":feature-device"))
implementation(project(":feature-inspection"))
}
优点:
-
依赖更加明确
-
可以按需引入
-
Feature 边界更清晰
缺点:
-
App 组装代码更多
-
iOS 仍然需要单独的 Framework 聚合模块
推荐方式
可以采用:
Android App
按需依赖 Feature 或 sharedUI
iOS App
通过 sharedLogic 聚合 Framework 接入
两个平台不必为了形式统一,采用完全相同的 Gradle 依赖入口。
架构一致指的是业务边界一致,不是每个平台的构建方式必须完全相同。
二十三、sharedUI 应该依赖谁?
如果使用 Compose Multiplatform:
sharedUI
可以依赖:
feature-auth
feature-device
feature-inspection
core-model
core-result
例如:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(project(":feature-auth"))
implementation(project(":feature-device"))
implementation(project(":feature-inspection"))
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
}
}
}
依赖方向应该是:
sharedUI
│
▼
feature
│
▼
core
不能反过来:
feature-device
依赖 sharedUI
业务层不应该知道页面使用 Compose、SwiftUI 还是 Flutter。
二十四、平台适配模块应该怎么依赖公共模块?
以 Android 扫码为例:
android-qrcode
可以依赖:
core-qrcode
core-model
dependencies {
implementation(project(":core-qrcode"))
implementation(project(":core-model"))
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.mlkit.barcode.scanning)
}
关系是:
android-qrcode
│
├── CameraX
├── ML Kit
└── core-qrcode
其中:
core-qrcode
负责 ScanParser、ScanResult、ScanValidator
android-qrcode
负责 CameraX、权限、生命周期和扫码 UI
core-qrcode 不能反向依赖 android-qrcode。
二十五、core-network 应该依赖 feature 吗?
不应该。
错误关系:
core-network
│
▼
feature-device
这意味着网络基础层知道设备业务。
更合理的是:
feature-device
│
▼
core-network
core-network 只负责:
-
HttpClient
-
BaseResponse
-
请求配置
-
通用 Header
-
网络错误映射基础
-
重试策略
-
Token Header 获取
它不应该包含:
DeviceApi
LoginApi
InspectionApi
这些应该分别放在具体 Feature:
feature-device/data/remote
feature-auth/data/remote
feature-inspection/data/remote
二十六、core-storage 应该依赖 feature 吗?
同样不应该。
core-storage 可以定义:
interface TokenStorage
interface KeyValueStorage
interface FileStorage
但不应该定义:
DeviceDraftManager
InspectionCacheManager
UserRepository
Feature 自己的数据应该由 Feature 自己管理。
例如:
feature-inspection
│
▼
core-storage
而不是:
core-storage
知道 InspectionForm
二十七、依赖注入怎么适配多模块?
拆成多模块后,每个 Feature 可以提供自己的依赖注册入口。
例如:
class AuthFeatureFactory(
private val client: HttpClient,
private val tokenStorage: TokenStorage,
private val logger: AppLogger
) {
fun createLoginUseCase(): LoginUseCase {
val remoteDataSource =
AuthRemoteDataSource(client)
val repository =
AuthRepositoryImpl(
remoteDataSource =
remoteDataSource
)
return LoginUseCase(
repository = repository,
tokenStorage = tokenStorage,
validator = LoginValidator(),
logger = logger
)
}
}
设备模块:
class DeviceFeatureFactory(
private val client: HttpClient,
private val database: AppDatabase
) {
fun createGetDeviceListUseCase():
GetDeviceListUseCase {
val remote =
DeviceRemoteDataSource(client)
val local =
DeviceLocalDataSource(database)
val repository =
DeviceRepositoryImpl(
remoteDataSource = remote,
localDataSource = local
)
return GetDeviceListUseCase(
repository = repository
)
}
}
最后由:
sharedLogic
androidApp
iosApp
完成总组装。
也可以使用支持 KMP 的依赖注入框架,但依赖方向仍然不能因为使用了 DI 而被打乱。
二十八、测试工具要不要单独拆模块?
当多个 Feature 都需要:
-
Fake Logger
-
Fake TokenStorage
-
Fake HttpClient
-
测试数据构造器
-
Coroutine Test 工具
可以建立:
core-testing
例如:
core-testing/
└── src/commonMain/
├── FakeLogger.kt
├── FakeTokenStorage.kt
├── TestDeviceFactory.kt
└── TestResultFactory.kt
Feature 测试依赖:
commonTest.dependencies {
implementation(project(":core-testing"))
}
需要注意:
生产代码
不能依赖 core-testing
只有测试 Source Set
才能依赖 core-testing
否则 Fake 实现可能进入正式构建。
二十九、Module 太多后,Gradle 配置会重复怎么办?
当每个 KMP Module 都要重复配置:
Android namespace
compileSdk
minSdk
iOS Targets
编译选项
测试依赖
协程依赖
序列化插件
可以考虑建立:
build-logic
并编写 Convention Plugin。
例如:
build-logic/
└── convention/
├── KmpLibraryConventionPlugin
├── KmpFeatureConventionPlugin
└── AndroidPlatformLibraryConventionPlugin
Feature 的 build.gradle.kts 最终可以简化成:
plugins {
id("sqx.kmp.feature")
alias(libs.plugins.kotlinSerialization)
}
再只保留该模块自己的依赖。
Android 官方也提供了针对 KMP 配置编写自定义 Gradle 插件的指导,可以通过 Kotlin Multiplatform Gradle Extension 统一多个 KMP Module 的配置。
但项目刚开始只有三四个 Module 时,不必过早编写复杂构建插件。
可以等重复配置真正出现后再提取。
三十、如何从单个 sharedLogic 平滑拆分?
不要一次把整个 sharedLogic 拆成二三十个 Module。
推荐分四步。
第一步:先按 package 建立边界
原结构:
sharedLogic/
├── core/model
├── core/result
├── core/network
├── feature/auth
├── feature/device
└── feature/inspection
先保证 package 之间的依赖方向是合理的。
第二步:先拆最底层模块
优先拆:
core-model
core-result
因为它们:
-
依赖少
-
业务稳定
-
被大量上层使用
-
拆分风险低
第三步:拆技术基础模块
继续拆:
core-network
core-storage
core-platform
这一步要检查:
-
是否反向依赖 Feature
-
是否暴露 Ktor、Room 等具体类型
-
平台实现放在哪个 Source Set
第四步:最后拆 Feature
当基础层稳定后,再拆:
feature-auth
feature-device
feature-inspection
最后让 sharedLogic 变成纯组装模块。
迁移过程中 Android App 可以同时依赖:
旧 sharedLogic 包
+
新拆出的 KMP Module
不需要一次完成全部迁移。
三十一、不要为了组件化拆得过细
多模块不是越多越好。
下面这种结构可能过度拆分:
device-model
device-repository-api
device-repository-impl
device-usecase-list
device-usecase-detail
device-remote
device-local
device-mapper
对于只有几千行代码的项目,这会带来:
-
Gradle 配置增加
-
模块导航困难
-
依赖关系复杂
-
构建脚本维护成本增加
-
小改动需要跨多个 Module
Android 官方模块化指南也强调,并不存在适合所有项目的唯一拆分方式,模块化需要根据项目规模、团队结构、复用需求和构建成本选择合适粒度。
可以先使用:
feature-device
同时包含设备 Domain 和 Data。
只有在确实需要:
-
单独发布
-
隐藏实现
-
多种实现
-
跨 Feature 只暴露契约
-
团队独立维护
时,再拆成:
feature-device-api
feature-device-impl
三十二、常见错误总结
错误一:commonMain 依赖 Android Module
commonMain.dependencies {
implementation(project(":android-map"))
}
问题:
iOS 无法解析 Android 依赖
正确方式:
androidMain.dependencies {
implementation(project(":android-map"))
}
错误二:底层 core 反向依赖 feature
core-network
依赖 feature-auth
正确方向:
feature-auth
依赖 core-network
错误三:所有依赖都使用 api
结果:
-
上层可以看到大量内部实现
-
模块边界失效
-
编译影响扩大
-
Framework API 膨胀
正确原则:
默认 implementation
公共签名确实暴露依赖类型
再使用 api
错误四:所有模型都塞进 core-model
结果:
core-model
变成整个项目的数据垃圾场
正确原则:
只有真正跨 Feature 的稳定模型才下沉到 core。
错误五:Feature 直接访问其他 Feature 的 Data 层
例如:
feature-inspection
依赖
feature-device/data/DeviceRemoteDataSource
正确方式:
-
依赖 Domain 契约
-
拆 API Module
-
或交给上层协调器组合
错误六:每个 KMP Module 都声明所有平台
如果某个模块只服务 Android:
android-qrcode
就不必强行声明 iOS 和 Web Target。
错误七:iOS 集成多个零散 Framework
项目内部可以有多个 KMP Gradle Module,但对 iOS 可以通过一个聚合 Framework 提供统一出口。
不要让 Xcode 同时管理大量内部 Kotlin Framework,增加集成复杂度。
错误八:一开始就拆几十个 Module
正确路径:
先单模块分层
再拆 core
再拆 feature
最后引入 build-logic
三十三、设备巡检 Demo 推荐依赖图
最终可以采用:
core-model
无业务模块依赖
core-result
无业务模块依赖
core-logging
依赖 core-model(可选)
core-storage
依赖 core-model、core-result
core-network
依赖 core-result、core-storage、core-logging
core-qrcode
依赖 core-result
core-map
依赖 core-model
feature-auth
依赖 core-model、core-result、
core-network、core-storage
feature-device
依赖 core-model、core-result、
core-network、core-storage、core-map
feature-inspection
依赖 core-model、core-result、
core-network、core-storage
sharedLogic
依赖并组装全部 Feature
sharedUI
依赖 Feature 和 sharedLogic
android-qrcode
依赖 core-qrcode
android-map
依赖 core-map、feature-device
androidApp
依赖 sharedUI、sharedLogic、
android-qrcode、android-map
iosApp
集成 SharedLogic Framework
并实现 iOS 扫码和地图能力
图形化表示:
androidApp
│
┌─────────────┼─────────────┐
│ │ │
sharedUI android-qrcode android-map
│ │ │
▼ ▼ ▼
sharedLogic core-qrcode core-map
│ │
┌─────────┼──────────┐ │
│ │ │ │
auth device inspection │
│ │ │ │
└─────────┼──────────┴────────────────┘
│
┌─────────┼──────────────────┐
│ │ │
network storage platform
│ │ │
└─────────┼──────────────────┘
│
┌──────┴──────┐
│ │
model result
三十四、总结
KMP 多模块组件化,本质上是在两个维度上同时划分边界。
第一维是:
Module
负责业务和技术职责边界
第二维是:
Source Set
负责平台边界
例如:
feature-device
是设备业务边界
feature-device/commonMain
是公共设备业务
feature-device/androidMain
是 Android 设备实现
feature-device/iosMain
是 iOS 设备实现
KMP Module 之间可以在 commonMain 中建立项目依赖:
commonMain.dependencies {
implementation(project(":core-network"))
}
Kotlin Gradle 插件会为不同平台选择兼容的依赖变体。
平台专属依赖则放在对应 Source Set:
androidMain.dependencies {
implementation(project(":android-qrcode"))
}
依赖方向应该始终保持:
App / UI
│
▼
Feature
│
▼
Core
而不是:
Core
反向依赖
Feature 或 UI
api 和 implementation 的选择原则是:
公共签名暴露依赖类型
→ api
只在内部实现使用
→ implementation
对于 iOS,可以保留多个内部 KMP Module,但通过:
sharedLogic
组装成一个统一 Framework。
最终可以用一句话概括:
Android 组件化用 Module 划分单平台职责,KMP 多模块组件化则同时使用 Module 划分业务边界、Source Set 划分平台边界。
更重要的是:
多模块不是为了增加目录和 Gradle 文件,而是为了让不该互相依赖的代码,在构建层面真正无法互相依赖。
当项目规模较小时,可以先在一个 sharedLogic 内建立清晰 package。
当 Feature、团队和复用需求逐渐增加后,再平滑拆成:
core-*
feature-*
platform-*
shared*
app*
这样既能保留 Android 组件化经验,也能真正形成一套可以同时服务 Android、iOS、Web 和其他终端的 KMP 跨平台底座。
下一篇预告
下一篇将进入本系列最后一篇:
《KMP 与 CMP 的关系:到底要不要共享 UI?》
我们会重点讲清楚:
-
KMP 和 CMP 到底是什么关系
-
只共享业务逻辑是否已经足够
-
Android Compose 页面能否直接迁到 CMP
-
sharedLogic和sharedUI应该如何依赖 -
登录页、列表页、表单页为什么适合共享
-
地图、扫码、相机、WebView 为什么不建议强行共享
-
CMP 与 SwiftUI、UIKit 如何混合
-
CMP 与 Android 原生 View 如何混合
-
KMP + CMP 和 KMP + Flutter 应该如何选择
-
设备巡检项目最终推荐采用哪种 UI 路线
下一篇会完成整个系列从:
Android 组件化
到:
KMP 业务底座
再到:
是否进一步共享 UI
的完整闭环。
更多推荐




所有评论(0)