Android/Gradle 项目中的管理依赖

141 阅读11分钟

在 Android/Gradle 项目中,管理依赖和共享构建逻辑主要有这四种方式。它们不是完全互斥的,而是常常被组合使用。


1. config.gradle (传统 ext 方式)

这是最传统的方式,通过在根项目的 build.gradle 中或一个独立的 config.gradle 文件中定义 ext 代码块来集中管理版本号。

示例 (config.gradle):

//groovy
ext {
    versions = [
        kotlin_version: "1.9.0",
        core_ktx_version: "1.12.0"
    ]
    libraries = [
        kotlin_stdlib: "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin_version}",
        core_ktx: "androidx.core:core-ktx:${versions.core_ktx_version}"
    ]
}

在根 build.gradle 中 apply from: "config.gradle"

核心原理:Groovy 脚本的简单注入与合并。

  1. 脚本应用 (Application) :当你在根 build.gradle 中使用 apply from: 'config.gradle' 时,Gradle 会在配置阶段 (Configuration Phase)  简单地读取并执行 config.gradle 文件中的 Groovy 代码。
  2. 属性注入config.gradle 中的 ext { ... } 代码块会在根项目的 Project 对象上定义一个名为 ext 的额外属性集。这些属性(如 versionslibraries)被存储为键值对。
  3. 作用域解析:Gradle 项目有一个重要的特性:根项目的扩展属性对其所有子项目是可见的。因此,在子项目(如 app)的 build.gradle 中,你可以通过 rootProject.ext.versions.kotlin 或简写的 rootProject.versions.kotlin 来访问这些属性。
  4. 字符串替换:最终,implementation rootProject.ext.libraries.core_ktx 这行代码在配置阶段被解析时,其实只是一个复杂的字符串拼接过程。Gradle 会先找到 rootProject.ext.libraries 这个 Map,再取出 core_ktx 对应的字符串值,然后将结果拼接成完整的依赖坐标 androidx.core:core-ktx:1.12.0

简单比喻:就像你在一个公共笔记本(config.gradle)上写下了所有配方(依赖),每个厨师(子项目)需要时都跑过来看一眼,然后把配方抄回去(字符串替换)。

优点:

  • 简单直观:学习成本几乎为零,适合所有水平的开发者。
  • 兼容性好:适用于所有版本的 Android Gradle Plugin 和 Gradle。
  • 集中管理:确实解决了依赖版本散落各处的问题。

缺点:

  • 无任何工具支持没有代码提示、没有自动补全、无法跳转定义,容易写错字符串。
  • 重构困难:重命名一个依赖需要手动全局查找替换。
  • 类型不安全:完全是字符串拼接,容易出错。

使用场景:

  • 小型或老旧项目,不希望引入任何新概念。
  • 作为向更先进方式迁移的临时步骤。

推荐度:  ★☆☆☆☆ (不推荐用于新项目)


2. buildSrc

这是一个特殊的 Gradle 模块,其中的 Kotlin/Java 代码可以被项目中的所有构建脚本直接访问。它常被用来编写约定插件(Convention Plugins)和集中管理依赖。

核心原理:Gradle 将其识别并优先编译为一个特殊的、隐藏的依赖项。

  1. 特殊模块识别:Gradle 在构建初始化阶段会自动识别项目根目录下名为 buildSrc 的目录,并将其作为一个独立的、内置的复合构建来处理。

  2. 优先编译:在开始配置和编译主项目之前,Gradle 会先编译 buildSrc 模块。这个过程包括:

    • 解析 buildSrc 自己的 build.gradle.kts 和依赖。
    • 编译 buildSrc/src/main/kotlin 下的所有 Kotlin/Java 代码。
    • 将编译后的输出(一个 JAR 文件)放入 Gradle 的构建脚本类路径 (build script classpath)  中。
  3. 对主项目可见:正因为 buildSrc 的产出物被放入了构建脚本的类路径,所以主项目及其所有子项目的 build.gradle.kts 文件都可以像导入普通库一样,直接访问 buildSrc 中编译好的类和对象(例如 Dependencies 对象)。IDE 也因此能够提供完美的代码提示。

  4. 缓存失效的根源:任何对 buildSrc 源码的修改,都会导致 Gradle 认为构建脚本的类路径发生了变化。根据安全原则,Gradle 必须使整个主项目的配置缓存失效,因为配置阶段的输出可能依赖于新的类路径。这就导致了下一次构建几乎是全量的。

简单比喻buildSrc 是一个为整个构建过程提供专用工具的优先建造的工具工厂。主项目还没动工前,必须先把这个工具工厂建好。一旦工具工厂改了设计(代码变更),之前根据旧工具生产的所有半成品(缓存)都被认为可能不兼容,必须清理掉重来。

优点:

  • 卓越的开发体验完美的代码提示、自动补全、跳转定义和重构支持,因为它是编译好的代码。
  • 类型安全:基于 Kotlin/Java,编译时就能发现错误。
  • 能力强大:不仅可以管理依赖,还可以编写复杂的构建逻辑和插件。

缺点:

  • 对构建缓存极不友好这是它的致命缺点。任何对 buildSrc 的修改(甚至只是加个空格)都会导致整个项目的构建缓存失效,下一次构建几乎等同于全量清洁构建,非常缓慢。
  • 依赖更新需要同步:修改依赖后需要等待 buildSrc 模块本身编译完成。

使用场景:

  • 中小型项目,其中构建逻辑相对稳定,不经常更改依赖。
  • 开发者极度看重 IDE 支持,且可以接受偶尔的缓存失效带来的编译延迟。

推荐度:  ★★★☆☆ (可以使用,但有明显短板)


3. Version Catalogs (版本目录) - TOML

这是 Gradle 官方推出的新一代依赖管理方案,通过一个标准的 libs.versions.toml 文件来定义所有依赖。

示例 (gradle/libs.versions.toml):

//toml
[versions]
kotlin = "1.9.0"
core-ktx = "1.12.0"

[libraries]
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }

[plugins]
android-application = { id = "com.android.application", version = "8.1.0" }

核心原理:Gradle 在配置阶段早期预解析标准化 TOML 文件,并生成类型安全的访问器。

  1. 文件定位与预解析:在构建的配置阶段非常早的时期,Gradle 就会在约定好的位置(gradle/libs.versions.toml)查找 TOML 文件。
  2. 模型构建:Gradle 会解析这个 TOML 文件,将其内容结构化为一个内部的依赖目录模型 (Catalog Model) 。这个模型理解 [versions][libraries][bundles][plugins] 这些概念及其之间的关系。
  3. 生成类型安全访问器:基于这个模型,Gradle 会在内存中动态生成类型安全的代码访问器。这就是为什么你可以在 build.gradle.kts 中直接使用 libs.core.ktx 这样的语法。libs 不是一个真实存在的对象,而是 Gradle 根据 TOML 内容为你“虚拟”出来的一个代理对象。
  4. IDE 集成:Android Studio 等其他 IDE 同样会解析这个 TOML 文件,并独立地提供代码补全和提示功能,而不需要等待 Gradle 编译任何东西。
  5. 缓存友好:修改 TOML 文件只会导致 Gradle 重新解析该文件并更新内部的目录模型。这个过程非常轻量,不会污染或使任务输出缓存失效,因为缓存机制知道任务的输出只依赖于具体的依赖坐标字符串,而不是生成这个字符串的 TOML 文件本身。

简单比喻:Version Catalog 是一个中央注册表。Gradle 和 IDE 都学会了如何查阅这个注册表。当你写 libs.core.ktx 时,你是在说“去注册表里查一下 core.ktx 对应的具体地址是什么”,而不是直接写死地址。改注册表内容不需要把整个城市重建。

优点:

  • 官方标准:是 Gradle 的未来,会有最好的长期支持和生态集成。
  • 出色的 IDE 支持:Android Studio Hedgehog (2023.1.1) 及以上版本为 TOML 文件提供了代码补全、提示和跳转
  • 构建缓存友好:修改 TOML 文件不会导致严重的缓存失效问题,性能远优于 buildSrc
  • 共享能力强:可以通过包含目录(Catalog Bundles)  功能一键引入一组相关的依赖。
  • 支持多项目:可以轻松地在多个项目间共享同一个 Catalog。

缺点:

  • 学习新语法:需要学习 TOML 文件的结构。
  • 能力限定:它只能用于管理依赖版本,不能像 buildSrc 那样编写任意的构建逻辑或插件。
  • IDE 支持仍需完善:虽然新版 AS 已经很好,但可能仍不如 buildSrc 里的 Kotlin 代码体验那么完美。

使用场景:

  • 所有新项目的依赖管理首选方案
  • 替代 config.gradle 和 buildSrc 的依赖管理功能。

推荐度:  ★★★★★  (对于依赖管理部分,强烈推荐)


4. Composing Builds (复合构建)

这是一种更彻底的模块化方式。它将构建逻辑剥离到一个完全独立的 Gradle 项目(通常叫 build-logic)中,然后被主项目所“包含”和使用。

核心原理:将通常独立的多个 Gradle 构建组合在一起,其中一个构建的产出可以作为另一个构建的依赖。

  1. 构建隔离:你的 build-logic 目录是一个完全独立、自包含的 Gradle 项目。它有自己的 settings.gradle.kts,可以独立于主项目进行构建、测试和发布。
  2. 显式包含:在主项目的 settings.gradle.kts 中,通过 includeBuild("build-logic") 语句,你明确告诉 Gradle:“请将 build-logic 这个构建包含到当前构建中来”。
  3. 依赖替换 (Dependency Substitution) :这是复合构建的核心魔法。当 Gradle 解析主项目的依赖时,如果发现一个被请求的依赖(例如 id("com.mycompany.android.application")恰好与包含的构建(build-logic)所提供的依赖匹配,Gradle 就不会去远程仓库查找,而是直接路由到 build-logic 构建的产出物
  4. 并行配置与构建:Gradle 可以并行地配置主项目和被包含的 build-logic 项目。build-logic 编译出的插件 JAR 会被自动提供给主项目使用。
  5. 缓存隔离这是它优于 buildSrc 的关键。主项目和 build-logic 项目有各自独立的配置缓存任务输出缓存。修改 build-logic 的代码只会使 build-logic 自己的缓存失效,并触发其重新编译。主项目的配置缓存大部分情况下仍然有效,因为它只依赖于 build-logic 输出的接口(插件id和版本),而不关心其实现源码。

简单比喻:主项目是一个汽车组装厂,build-logic 是一个专业的发动机设计公司。通过战略合作(includeBuild),组装厂可以直接使用设计公司最新研发的发动机(插件),而无需等它上市(发布到仓库)。如果设计公司改进了发动机的内部零件(代码逻辑),只需要重新制造发动机本身,而不会影响组装厂里已经造好的车门和座椅(主项目缓存)。

优点:

  • 终极的构建缓存友好:修改构建逻辑不会破坏主项目的缓存,编译速度最快。
  • 极致隔离与复用:构建逻辑是一个独立项目,可以有自己的测试、依赖,甚至可以轻松复用到其他完全不相关的项目中。
  • 优秀的开发体验:和 buildSrc 一样,你可以在独立的 build-logic 模块中享受完整的 IDE 支持。
  • 官方推荐:Gradle 官方推荐的使用约定插件的方式。

缺点:

  • 复杂度最高:设置起来比另外几种方式都更复杂,需要理解复合构建的概念。
  • 有点“杀鸡用牛刀” :对于非常小型的项目,可能显得过于重型。

使用场景:

  • 中大型项目,需要高度可定制和可共享的构建逻辑(约定插件)。
  • 需要将一套构建规范应用到多个不同项目中。
  • 与 Version Catalogs 结合使用,作为最佳实践。

推荐度:  ★★★★★  (对于共享构建逻辑/约定插件部分,强烈推荐)


总结与最终推荐

总结对比

特性config.gradlebuildSrcVersion CatalogsComposing Builds
执行阶段配置阶段(脚本执行)优先于配置阶段(先编译)配置阶段早期(预解析)配置阶段(构建包含)
本质脚本变量注入特殊优先编译的模块标准化的元数据文件构建间的依赖替换
IDE 支持原理无,纯文本基于编译后的类原生解析 TOML 文件基于编译后的类
缓存影响无影响破坏主项目配置缓存几乎无影响隔离良好,影响最小
核心优势简单开发体验好官方标准、缓存友好极致性能与复用

希望这些原理性的解释能帮助你更深刻地理解它们的不同,从而做出最合适的选择。

最佳实践组合推荐:

对于 新项目 或 希望进行现代化改造的项目,推荐采用以下 组合拳

  1. 使用 Version Catalogs (libs.versions.toml) 管理所有依赖项。

    • 做什么:所有第三方库、插件版本都在这里定义。
    • 为什么:它是官方标准,IDE支持好,且对缓存友好。
  2. 使用 Composing Builds (一个独立的 build-logic 项目) 来编写和组织你的约定插件。

    • 做什么:在这里创建 AndroidApplicationConventionPluginAndroidLibraryConventionPluginKotlinAndroidConventionPlugin 等,将公共配置逻辑抽离到这里。
    • 为什么:它提供了最好的开发体验和构建性能,并且允许高级别的代码复用。
    • 如何使用依赖:在 build-logic 自己的 build.gradle.kts 中,你可以使用 libs 来引用你在 TOML 中定义的依赖,或者直接声明自己的依赖。

示例:主项目 build.gradle.kts 最终变得非常简洁:

kotlin

// app/build.gradle.kts
plugins {
    id("com.mycompany.android.application") // 来自 build-logic 的约定插件
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.mycompany.awesomeapp" // 仅配置特有选项
}

dependencies {
    implementation(libs.core.ktx) // 来自 version catalogs
    implementation(libs.coil.compose)
}

这个组合让你同时享受了 标准化依赖声明 和 模块化、高性能的构建逻辑 的所有好处