模块边界划分策略:根据功能域拆分、避免循环依赖、明确模块接口

原理:为什么需要清晰的模块边界

在 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 注入。

边界划分流程

  1. 识别功能域:绘制当前代码的调用链路,圈出强内聚的业务包(如登陆、订单、推送)。
  2. 抽取接口模块(api module):每个功能域建立一个 myfeature-api 纯接口模块,只包含抽象类、接口、数据类(无实现依赖)。
  3. 检查循环依赖:使用 gradle dependencies 验证模块依赖树,确保没有跨模块双向依赖。
  4. 约束依赖方向:规定业务模块只能依赖 api 模块或基础工具模块,严禁业务模块之间直接 implementation project 引用。
  5. 验证 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 概率。

模块边界划分示意

功能域拆分与接口隔离模型 功能域 A 登录 / 注册 (实现模块) 功能域 B 订单 / 支付 (实现模块) 功能域 C 推送 / 通知 (实现模块) 公共接口层 (api 模块) LoginService / OrderService / PushService 基础工具 / 网络 / 存储 ⚠ 禁止模块间直接依赖 避免循环依赖

常见错误与调试

  • 错误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)审计循环依赖,保持边界清晰。