三层模块依赖视角:彻底搞懂api和testImplementation的区别
Android项目拆分为多模块后,依赖管理就成了绕不开的核心问题。比如当项目存在"A(基础工具)→ B(业务封装)→ C(主应用)"的三层依赖时,用api还是testImplementation引入库,直接决定了"哪些模块能访问这个库"。今天就以这三个模块为载体,用真实配置和代码案例,把两者的区别讲透,让多模块依赖配置不再踩坑。
一、先明确模块关系:A-B-C三层依赖模型
为了让案例更贴近实际开发,先定义三个模块的职责和依赖关系,后续所有测试都基于这个模型展开: 
- 模块A(base-utils) :基础工具模块,提供JSON解析、网络请求等通用能力,引入Gson、Retrofit等基础库。
- 模块B(business-core) :业务封装模块,依赖模块A,封装用户、订单等业务接口,对外提供业务能力。
- 模块C(app) :主应用模块,依赖模块B,是最终的应用入口,调用业务能力实现界面展示。
核心问题:当模块A引入某个库时,模块B和模块C能否访问?api和testImplementation的选择,就是回答这个问题的关键。
二、api:贯穿A-B-C的"传递性主依赖"
api的核心特性是传递性 ------模块A用api引入的库,会像"接力棒"一样传递给依赖它的模块B,再传递给依赖B的模块C,三层模块的主代码都能直接使用这个库。
实战案例:模块A用api引入Gson
需求:模块A封装JSON解析工具,需要引入Gson,且模块B和模块C可能直接使用Gson解析JSON。
步骤1:模块A的配置与代码
在模块A的build.gradle中用api引入Gson,并封装解析工具:
            
            
              arduino
              
              
            
          
          // 模块A(base-utils)的build.gradle
dependencies {
    // 用api引入Gson,具备传递性
    api 'com.google.code.gson:gson:2.10.1'
}
            
            
              kotlin
              
              
            
          
          // 模块A的主代码:封装JSON解析工具(src/main/java/com/example/base/JsonUtils.kt)
object JsonUtils {
    // 直接使用api引入的Gson
    fun <T> parseJson(json: String, clazz: Class<T>): T {
        return Gson().fromJson(json, clazz)
    }
}步骤2:模块B的配置与代码
模块B依赖模块A,无需重复引入Gson,既能使用模块A的工具,也能直接用Gson:
            
            
              java
              
              
            
          
          // 模块B(business-core)的build.gradle
dependencies {
    // 仅依赖模块A,无需引入Gson
    implementation project(':base-utils')
}
            
            
              kotlin
              
              
            
          
          // 模块B的主代码:封装用户业务(src/main/java/com/example/business/UserRepository.kt)
object UserRepository {
    // 场景1:使用模块A传递的JsonUtils工具
    fun getUserFromJson(json: String): User {
        return JsonUtils.parseJson(json, User::class.java)
    }
    // 场景2:直接使用模块A传递的Gson(无需额外引入)
    fun getUserListFromJson(json: String): List<User> {
        val type = object : TypeToken<List<User>>() {}.type
        return Gson().fromJson(json, type) // 直接使用Gson,无报错
    }
}步骤3:模块C的配置与代码
模块C依赖模块B,同样无需引入Gson,能直接使用模块A传递的Gson和模块B的业务能力:
            
            
              java
              
              
            
          
          // 模块C(app)的build.gradle
dependencies {
    // 仅依赖模块B,无需引入Gson或模块A
    implementation project(':business-core')
}
            
            
              kotlin
              
              
            
          
          // 模块C的主代码:主界面逻辑(src/main/java/com/example/app/MainActivity.kt)
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val userJson = "{\"id\":1,\"name\":\"张三\"}"
        val userListJson = "[{\"id\":1},{\"id\":2}]"
        // 场景1:使用模块B的业务能力
        val user = UserRepository.getUserFromJson(userJson)
        // 场景2:使用模块B传递的Gson(间接来自模块A)
        val userList = Gson().fromJson(userListJson, object : TypeToken<List<User>>() {}.type)
        // 场景3:使用模块A传递的JsonUtils(间接来自模块B)
        val userFromUtils = JsonUtils.parseJson(userJson, User::class.java)
    }
}关键结论:api的传递性范围
模块A用api引入的Gson,传递路径为A→B→C,三层模块的主代码都能直接访问,无需重复引入,适合"基础库需要被全链路模块使用"的场景。
三、testImplementation:仅禁锢在当前模块的"非传递性测试依赖"
testImplementation的核心特性是非传递性+仅测试可见 ------模块A用testImplementation引入的测试库,仅模块A的测试代码能使用,模块B和模块C的测试代码都无法访问,主代码更无法使用。
实战案例:模块A用testImplementation引入JUnit
需求:模块A需要写单元测试,引入JUnit框架,模块B和模块C的测试不依赖此框架。
步骤1:模块A的配置与测试代码
在模块A的build.gradle中用testImplementation引入JUnit,测试代码可正常使用:
            
            
              arduino
              
              
            
          
          // 模块A(base-utils)的build.gradle
dependencies {
    api 'com.google.code.gson:gson:2.10.1'
    // 用testImplementation引入JUnit,仅测试可见
    testImplementation 'junit:junit:4.13.2'
}
            
            
              kotlin
              
              
            
          
          // 模块A的测试代码(src/test/java/com/example/base/JsonUtilsTest.kt)
import org.junit.Test
import org.junit.Assert.*
class JsonUtilsTest {
    // 正常使用testImplementation引入的JUnit
    @Test
    fun testParseJson() {
        val json = "{\"id\":1,\"name\":\"张三\"}"
        val user = JsonUtils.parseJson(json, User::class.java)
        assertEquals("张三", user.name) // JUnit断言正常生效
    }
}步骤2:模块B的测试代码(无法访问JUnit)
模块B依赖模块A,但无法访问模块A用testImplementation引入的JUnit,需自行引入才能使用:
            
            
              java
              
              
            
          
          // 模块B(business-core)的build.gradle
dependencies {
    implementation project(':base-utils')
    // 若模块B需要测试,需自行引入JUnit,无法复用模块A的
    // testImplementation 'junit:junit:4.13.2'
}
            
            
              kotlin
              
              
            
          
          // 模块B的测试代码(src/test/java/com/example/business/UserRepositoryTest.kt)
import org.junit.Test // 未自行引入时,此处报错
import org.junit.Assert.*
class UserRepositoryTest {
    @Test // 未引入JUnit时,注解报错
    fun testGetUserFromJson() {
        val json = "{\"id\":1,\"name\":\"张三\"}"
        val user = UserRepository.getUserFromJson(json)
        assertEquals("张三", user.name)
    }
}步骤3:模块C的测试代码(同样无法访问)
模块C依赖模块B,即使模块A引入了JUnit,模块C的测试代码也完全无法访问,必须自行引入:
            
            
              java
              
              
            
          
          // 模块C(app)的build.gradle
dependencies {
    implementation project(':business-core')
    // 模块C测试需自行引入JUnit
    testImplementation 'junit:junit:4.13.2'
}关键结论:testImplementation的范围限制
模块A用testImplementation引入的JUnit,仅能在模块A的测试代码中使用,传递路径中断于A模块,模块B和C无法复用,适合"测试工具仅当前模块需要"的场景。
四、A-B-C模块视角的核心区别对比表
| 对比维度 | api(以模块A引入Gson为例) | testImplementation(以模块A引入JUnit为例) | 
|---|---|---|
| 模块A主代码可见性 | 可见(可直接用Gson) | 不可见(主代码无法用JUnit) | 
| 模块A测试代码可见性 | 可见(测试代码也能使用Gson) | 可见(仅测试代码能用JUnit) | 
| 模块B主代码可见性 | 可见(传递性,无需重复引入) | 不可见(完全无法访问) | 
| 模块B测试代码可见性 | 可见(传递性,测试代码也能用) | 不可见(需自行引入) | 
| 模块C主代码可见性 | 可见(传递性,A→B→C) | 不可见(完全无法访问) | 
| 传递性路径 | A→B→C(全链路传递) | 无传递性(仅A模块测试可用) | 
| 典型使用场景 | 基础库、通用工具(需全链路使用) | 测试框架、模拟工具(仅当前模块测试用) | 
五、延伸避坑:api和implementation的选择(多模块必备)
多模块开发中,很多人会混淆api和implementation,这里补充关键区别:implementation是非传递性主依赖 ,模块A用implementation引入的库,仅模块A主代码可见,模块B和C无法直接使用。
比如模块A用implementation引入Gson:
            
            
              arduino
              
              
            
          
          // 模块A的build.gradle
dependencies {
    implementation 'com.google.code.gson:gson:2.10.1' // 替换为implementation
}此时模块B和C只能使用模块A封装的JsonUtils工具,无法直接使用Gson------这能避免依赖过度暴露,减少构建时间,是大多数主代码场景的首选。
- 多模块配置口诀 *:主依赖优先用implementation,需全链路共享用api;测试依赖固定用testImplementation,各模块独立引入。
六、一句话总结全文
在A-B-C三层模块依赖中,api是全链路传递的主依赖,A模块引入后B、C模块主代码都能直接使用,适合共享基础库;testImplementation是仅当前模块测试可见的非传递依赖,A模块引入后B、C模块完全无法复用,适合独立测试工具,主依赖优先用implementation更高效。