前言

上一篇我们已经在单个 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

的完整闭环。

Logo

一站式 AI 云服务平台

更多推荐