第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 依赖自动计算编译顺序。一个典型的依赖关系图如下所示:

模块编译顺序与依赖流动 :app (主应用) :feature:home :feature:login :feature:profile :libraries:net :libraries:ui-common :data:repository 编译顺序::library → :data → :feature → :app(依赖方后编译)

在 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 迁移步骤示例

  1. 在 Git 中创建新分支 refactor/multi-module,备份单体项目。
  2. 创建根项目的 settings.gradle.kts,先只包含 :app。
  3. 提取第一个公共模块 :libraries:net,将网络相关类移入,并在 :app 中改为 implementation(project(":libraries:net"))。
  4. 执行 ./gradlew :app:assembleDebug 验证编译通过。
  5. 重复提取功能模块(如 :feature:home),每步都运行全量编译和单元测试。
  6. 调整包名后,同步更新 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 个以上类,否则项目管理成本过高。
注意:迁移过程中不要同时修改模块代码逻辑和降级依赖版本,每次只做一个变更,否则难以定位回归问题。