第2章 单体项目到多模块迁移:逐步拆分策略、包结构调整、编译顺序
2.1 为什么需要逐步迁移
将单体 Gradle 项目拆分为多模块体系并非一次性重构,而是应该遵循“剥离-依赖-测试-集成”的渐进式路径。直接重写所有模块会导致版本冲突、资源错误和编译中断。正确的策略是保留单体稳定分支,在功能点级别逐步提取可独立编译的模块,并持续集成验证。
核心原则:每次迁移只改动一个模块,确保单体项目在该模块提取后依然能通过编译和测试,再进入下一个模块。
2.2 拆分策略
按照常见 Gradle 多模块工程模式,推荐三种拆分维度:
| 维度 | 适用场景 | 典型模块名 |
|---|---|---|
| 按功能层 | 业务逻辑层、数据层、UI层分离 | :core:domain, :data:repository, :ui:feature |
| 按业务单元 | 大型微服务或功能组庞杂的 App | :feature:home, :feature:profile, :feature:search |
| 按公共基础设施 | 网络、图片加载、日志等基础能力 | :libraries:networking, :libraries:imageloading |
2.3 包结构调整
单体项目的包路径通常为 com.example.app,所有类集中在此包下。迁移后,每个模块应拥有自己的专属包路径,防止包冲突。建议保持模块名与包前缀的映射关系:
单体路径: com/example/app/MainActivity.java
模块 :app ⇒ 包路径保持 com.example.app
模块 :feature:login ⇒ 包路径改为 com.example.app.login
这样既能保留原有内部引用关系,又能让 Android 资源 R 类在模块间独立生成,避免 R.id 冲突。
2.4 编译顺序与依赖配置
Gradle 根据模块间的 implementation 和 api 依赖自动计算编译顺序。一个典型的依赖关系图如下所示:
在 settings.gradle.kts 中声明所有模块:
// settings.gradle.kts
rootProject.name = "MyApp"
include(":app")
include(":feature:home")
include(":feature:login")
include(":feature:profile")
include(":libraries:net")
include(":libraries:ui-common")
include(":data:repository")
在 :app/build.gradle.kts 中声明对功能模块的依赖:
dependencies {
implementation(project(":feature:home"))
implementation(project(":feature:login"))
// 间接依赖:feature 模块内部再 declare :libraries:net
}
2.5 迁移步骤示例
- 在 Git 中创建新分支
refactor/multi-module,备份单体项目。 - 创建根项目的
settings.gradle.kts,先只包含:app。 - 提取第一个公共模块
:libraries:net,将网络相关类移入,并在:app中改为implementation(project(":libraries:net"))。 - 执行
./gradlew :app:assembleDebug验证编译通过。 - 重复提取功能模块(如
:feature:home),每步都运行全量编译和单元测试。 - 调整包名后,同步更新 ADB 资源引用和第三方库版本(通过
libs.versions.toml统一管理)。
2.6 调试与验证
使用 Gradle 自带的依赖分析任务检查依赖树:
./gradlew :app:dependencies --configuration implementation
输出会显示所有直接与传递依赖,帮助发现循环依赖或冗余模块。迁移前后建议对比构建耗时:
# 单体
./gradlew :app:assembleDebug --build-cache --profile
# 多模块(首次全量)
./gradlew assembleDebug --build-cache --profile
2.7 常见错误及解决
| 错误类型 | 现象 | 解决方案 |
|---|---|---|
| 循环依赖 | Gradle 报 Cycle detected | 检查模块间双向依赖,抽取公共接口模块 |
| R类冲突 | 资源 ID 无法解析 | 使用 android.namespace 区分模块,避免同资源名 |
| 版本不一致 | 运行时异常 NoClassDefFoundError | 通过版本目录 (libs.versions.toml) 统一所有模块的依赖版本 |
| 编译顺序错误 | 模块找不到依赖的类 | 检查 implementation vs api 作用域,公共库应使用 api |
2.8 性能与稳定性优化
- 增量编译:每个模块开启
kapt.incremental=kapt(Kotlin 注解处理)或android.enableR8.fullMode=true。 - 并行编译:在
gradle.properties中设置org.gradle.parallel=true,模块间无依赖时可同时编译。 - 模块粒度控制:不要将代码拆得过于零碎,建议每个模块至少包含 10 个以上类,否则项目管理成本过高。
注意:迁移过程中不要同时修改模块代码逻辑和降级依赖版本,每次只做一个变更,否则难以定位回归问题。