先确定共享什么,再决定拆多少模块
在 Kotlin Multiplatform 应用中,模块数量并不是架构质量的指标。更值得关注的是:修改一项业务时需要理解多少代码,平台差异是否被隔离,以及能否在清楚的边界上验证行为。
本文整理我在多平台项目中使用过的模块划分思路。下面的模块名称和依赖图是参考结构,不是新项目必须一次建齐的清单。
适合的读者:已经了解 Gradle 子项目与 KMP source set,正在为多个功能或多个平台整理代码的开发者。对于早期验证项目,一个应用模块加一个共享模块通常更容易维护;出现实际的复用、测试或协作需求时再拆分。
两种边界不要混在一起
| 边界 | 解决的问题 | 典型例子 |
|---|---|---|
| Gradle 模块 | 依赖关系、编译和职责分工 | :shared:core、:shared:data |
| KMP source set | 哪些平台共享哪些代码 | commonMain、iosMain、androidMain |
模块之间通过 implementation(project(...)) 等依赖声明连接;source set 的 dependsOn 描述共享代码关系,两者不是一回事。
例如,网络请求接口可以放在 commonMain,平台特有的能力放在对应平台 source set。公共代码只能使用所有相关目标都能使用的 API。优先使用 Kotlin 的默认 source set 层级,只有真实的平台分组需求才增加自定义层级。参见 source set 层级 和 项目结构中的依赖关系。
从职责划分共享层
我将共享基础能力整理为三个方向:
- design:主题、设计规范及跨平台 UI 组件。它不负责网络请求和持久化。
- core:共享接口、状态模型、协程相关约定等稳定抽象。避免逐渐变成无所不包的工具集合。
- data:数据获取、存储接口与模型转换。平台 SDK 的细节应留在实现侧。
业务代码依赖自己需要的能力,不通过一个巨大的聚合模块间接获得所有实现。Koin 可以协助装配对象,但依赖注入本身不会消除模块之间的编译依赖。
将可独立维护的能力拆出来
原有项目中涉及的能力较多,可以按使用场景理解,而不必逐项复制:
| 能力组 | 示例 | 拆分时重点检查 |
|---|---|---|
| 基础设施 | logger、network、connectivity-monitor、crashlytics | 平台支持是否一致,业务是否依赖具体 SDK |
| 多媒体 | kmplayer、record、screen-capture、imagecropper | 生命周期、资源释放和硬件能力差异 |
| 界面与交互 | webview、toast、country-picker、sdp | 组件是否真正跨业务复用 |
| 业务服务 | auth、payment、ai-generative | 接口边界和可测试性,敏感配置放在哪里 |
| 工具与集成 | permissions、filekit、imageloader、revenuecat | 是否需要统一封装,还是直接使用库更简单 |
有些实现只服务一个业务,将它留在业务模块里反而更清楚。判断标准是变化原因与使用范围,而不是“以后可能复用”。
让依赖方向可解释
下面用简化图表达原项目中的主要关系,箭头表示编译依赖:
这张图描述一种实现结构,并不意味着所有项目都应该让 AI 模块直接依赖数据实现。当替换实现或独立测试变得困难时,可以把所需接口提取到稳定的边界,由应用入口负责装配。
例如,在某个 KMP 模块中声明公共依赖:
示例省略插件和目标平台配置,需结合项目实际的 Kotlin、Compose 版本与目标平台使用。只有公共 API 确实向消费者暴露了另一个模块的类型时,才考虑使用 api 传递依赖。
渐进拆分的工作顺序
- 找出变化频率不同、可以单独描述职责的代码,先整理包边界。
- 给边界上的行为补测试,再移动到模块,避免边移动边改业务。
- 用版本目录统一依赖版本,检查目标平台与库的兼容范围。
- 检查依赖图是否有环、是否出现过度聚合的 core 模块。
- 分别测量干净构建和增量构建,再判断拆分是否改善了开发反馈。
可以用 module-graph 辅助观察依赖关系。图只是观察入口,不能替代对职责的判断。
如何验证拆分结果
共享逻辑在 commonTest 验证,平台适配分别测试,关键业务再做跨模块集成验证。测试任务名称随目标平台而变化,应先运行 ./gradlew tasks --all 查看项目实际提供的任务。
一次拆分至少应回答:调用方是否更容易使用?实现是否能独立替换?平台相关代码是否留在正确的位置?构建时间和发布产物有没有意外变化?
Gradle 模块化也不等于运行时按需加载。是否能减少启动成本,还取决于初始化逻辑、平台运行时与交付方式,不能仅凭拆了模块就承诺性能收益。