模块边界划分策略:根据功能域拆分、避免循环依赖、明确模块接口
原理:为什么需要清晰的模块边界
在 AGP 升级过程中,Android 工程通常会经历组件化或模块化重构。模块边界划分的核心目标是:
- 按功能域拆分:将紧密相关的业务逻辑、数据源、UI 片段聚合到同一个模块内部,减少跨模块的耦合。
- 避免循环依赖:模块 A 依赖模块 B,模块 B 又依赖模块 A,会导致编译死锁、增量编译失效、AGP 构建图计算异常。
- 明确模块接口:每个模块只暴露最小必要的 API(接口或抽象类),内部实现细节对外隐藏,降低 AGP 编译时的全量分析开销。
在 AGP 7.x / 8.x 中,构建系统会分析所有 module 之间的依赖关系图。循环依赖不仅会引起构建报错,还会让 --parallel 执行退化回串行,严重影响增量缓存命中率。
划分策略对比
| 策略 | 依据 | 优势 | 风险 |
|---|---|---|---|
| 按功能域拆分 | 业务领域(如登录、支付、搜索) | 高内聚、独立演进、AGP 模块图扁平 | 跨域调用需通过接口层,初期重构成本高 |
| 按架构层拆分 | 分层(data / domain / presentation) | 架构清晰,团队分工明确 | 易产生循环依赖(domain 反向依赖 data) |
| 混合策略 | 功能域 + 架构层 | 兼顾业务与结构 | 模块数量膨胀,AGP 同步耗时增加 |
推荐:在 AGP 升级周期中优先使用“按功能域拆分 + 统一接口层”模型。每个功能域模块只对外暴露
@Keep 标记的接口,内部实现通过 Dagger / Hilt 注入。
边界划分流程
- 识别功能域:绘制当前代码的调用链路,圈出强内聚的业务包(如登陆、订单、推送)。
- 抽取接口模块(api module):每个功能域建立一个
myfeature-api纯接口模块,只包含抽象类、接口、数据类(无实现依赖)。 - 检查循环依赖:使用
gradle dependencies验证模块依赖树,确保没有跨模块双向依赖。 - 约束依赖方向:规定业务模块只能依赖 api 模块或基础工具模块,严禁业务模块之间直接
implementation project引用。 - 验证 AGP 构建图:运行
./gradlew app:dependencies --configuration releaseCompileClasspath输出依赖树,确认无 cycle。
接口明确化示例
在 api 模块中定义接口,业务模块实现:
// login-api/src/main/java/com/example/login/api/LoginService.java
package com.example.login.api;
import androidx.annotation.NonNull;
public interface LoginService {
@NonNull
String getCurrentUserId();
boolean isLoggedIn();
}
业务模块只通过接口依赖,避免直接引用内部类:
// order/src/main/java/com/example/order/OrderManager.java
// 正确:只依赖 api 接口
import com.example.login.api.LoginService;
public class OrderManager {
private final LoginService loginService;
public OrderManager(LoginService loginService) {
this.loginService = loginService;
}
}
避免循环依赖的检查命令
在项目根目录执行:
./gradlew :app:dependencies --configuration compileClasspath | grep "cycle\|\->"
# 如果输出包含 "cycle" 或 "-->*", 表示存在循环
小技巧:在 AGP 8.1+ 版本中,如果构建报
Dependency cycle detected,可直接定位到循环的模块链。建议对所有 implementation 依赖使用 api 暴露的接口模块,大幅减少 cycle 概率。
模块边界划分示意
常见错误与调试
- 错误1:业务模块直接
implementation project(':login')而非implementation project(':login-api')。后果:编译时 AGP 强制分析 login 模块的全部依赖,降低缓存复用率。 - 错误2:接口模块内包含了具体实现类(如 Retrofit 实例)。后果:接口模块变得“不纯”,其他模块依赖时被迫拉入大量运行时库,触发 AGP 的
transform重跑。 - 错误3:接口变更未同步更新所有实现模块,导致 AGP 构建时出现
NoSuchMethodError或AbstractMethodError。
调试建议:使用 AGP 的
--profile 参数生成构建报告,查看每个模块的 Compile 耗时。如果发现有跨模块重复编译,优先检查是否违反了“接口模块只暴露抽象”原则。
性能与稳定性优化
- 为每个功能域创建独立的
-api模块后,AGP 的增量编译范围可缩小至改动的模块及其直接接口依赖方。 - 在
settings.gradle.kts中使用include(…)仅包含必要的 api 模块,避免未使用的功能域进入构建图。 - 定期执行
./gradlew :app:checkDependencies(需自定义 task)审计循环依赖,保持边界清晰。