Gradle依赖管理 & Kotlin DSL解析

公共组件库依赖管理

公共组件库项目采用了单projectmodule的模块化开发形式, 在这样的项目结构下, 如何去维护模块及外部依赖是一个我们不能回避的问题. 在组件库阶段一及阶段二的研发过程中, 我们遇到了以下与依赖相关的问题:

  1. 如何在开发过程中统一各组件模块中的依赖及版本
  2. 如何高效的解决, 在开发过程中依赖本地组件模块; 测试/发布过程中使用远端依赖的问题

针对问题一,可以采用通用的组件库,从而实现各个模块中外部依赖及版本统一. 当然目前依赖库还在试用阶段, 我们在使用的期间也踩过一些坑, 后面会详细跟大家分享.

针对问题二, 我们通过使用Gradle buildSrc定义模块动态依赖. 在CI/CD的基础上, 实现了根据开发/测试/发布阶段自动切换本地或远端依赖的功能.

1. 构建工具简史

Gradle是我们在安卓开发中最常用的构建工具, 自Android Studio发布以来, 他一直是默认的构建工具. 但是其实在Gradle之前, 还有很多知名的构建工具, 例如Java开发中常用的Maven等等.

其中最经典的当属Ant了, Ant使用的DSLxml, xml的进化来源于MakeFile构建的繁琐, 而xml特点是结构化且好理解, 这比写脚本插件简单多了, 所以迅速流行起来.

随着软件行业的迅速发展, 我们的产品功能越来越多, 业务越来越复杂, 开发团队也日益庞大, 这时候工程管理和工程的标准化问题就开始日益突出, 于是Maven诞生了. Maven很好的解决了依赖问题, 引入了标准依赖库对版本进行管理, 并且对工程的目录结构、构建生命周期都做了标准化定义, 极大的方便了工程管理及开发.

但是当Maven流行一段时间之后, 大家又发现了问题, xml逻辑简单是不错, 但是写起来太啰嗦, 而且扩展性不够, 此时, Gradle登场了. GradleMaven的基础上, 主要解决了两个问题:

  1. 用一种新的DSL, 让语法变的更简洁, 且支持扩展;
  2. 定义了扩展方便且不失标准的构建生命周期;

实际上Gradle发展至今, 早已超越了上面这两点, 而且还在不断的进化中, 比如buildSrc的诞生、kts的支持、KSP的演进等等.

2. Gradle依赖管理

Gradle的日常使用中, 依赖管理其实是非必须的. 但是随着项目的不断发展, 其中的依赖也会越来越多, 这个时候对项目依赖做一个统一的管理很有必要, 我们一般会有以下需求:

  1. 项目依赖统一管理, 在单独文件中配置
  2. 不同Module中的依赖版本号统一
  3. 不同项目中的依赖版本号统一

而随着Gradle版本迭代, 针对这些需求也有一些方案的更新:

  1. 最常见的是在全局属性ext中定义版本信息, 然后由各module去使用
  2. 使用buildSrc, 并在buildSrc中定义版本信息
  3. 使用includeBuild统一配置依赖版本

不过随着Gradle7.0的推出, 一个新的方式通过使用Catalog统一依赖版本, 它支持以下特性:

  1. 对所有module可见, 可统一管理所有module的依赖
  2. 支持声明依赖bundles, 即总是一起使用的依赖可以组合在一起
  3. 支持版本号与依赖名分离, 可以在多个依赖间共享版本号
  4. 支持在单独的libs.versions.toml文件中配置依赖
  5. 支持在项目间共享依赖

这也是我们在公共依赖库中使用的方案.

3. 组件库项目依赖

在组件库中, 我们使用了公共依赖库针对车机的android-30-v0.0.1-SNAPSHOT稳定版本:

// ../setting.gradle.kts
pluginManagement {
    ...
}

dependencyResolutionManagement {

    repositories {
        ...
    }

    versionCatalogs {

        val version = "android-30-v0.0.1-SNAPSHOT"

        // 官方依赖(包含android/kotlin等)
        create("official") {
            from("com.max.android.dependency:dependency-android:$version")
        }

        // 基础依赖(包含网络/数据/常用工具等)
        create("core") {
            from("com.max.android.dependency:dependency-core:$version")
        }
    }
}

...

值得注意的是, 组件库已经全面迁移至 Gradle Kotlin DSL , 已保证可以使用其自动补全的 功能 , 这点在依赖使用中真的有重要.

关于android-30-v0.0.1-SNAPSHOT这个公共依赖版本的来历有一个小插曲. 在此之前, 我们使用的是公共依赖库v0.0.5-SNAPSHOT的版本, 这个版本所有的依赖是基于android 33维护的. 我们开发阶段中由于都是在平板上进行调试问题没有暴露出来, 在阶段一需求提测后, 才发现用于测试的demo在台架上直接不能运行, 最终定位到是由于compile sdk指定版本过高而导致的. 随后我们便紧急降低了公共组件库的compile sdk版本与车机同步, 并针对该版本重新验证并维护了一个公共依赖库版本.

由此可见, 对于依赖管理的疏忽, 很有可能会产生预期之外的问题.

在引入了公共依赖库后, 我们在各module中引用依赖的方式就变为:

// ../module/build.gradle.ktx

...

dependencies {
    implementation(official.core.ktx)
    implementation(official.appcompat)
    implementation(official.google.material)
}

首先我们完全可以不用去关注使用依赖的版本号, 因为这部分已经在引用公共依赖库发布的VersionCatalog中定义好. 其次, 由于Gradle Kotlin DSL的自动补全特性的存在, 我们也很大程度上, 省去了记录依赖包名的必要.

4. 组件库动态依赖实现

不同于常规的应用开发, 组件库最终交付的是可供外部依赖引用的产物. 所以我们期望的是:

  1. 在开发阶段, 所有针对组件模块的依赖方式可以是本地依赖, 以保证对任意组件模块代码改动是实时生效的;
  2. 在测试阶段, 提交测试用的Demo应用及组件模块间的相关依赖方式必须是远端依赖, 可以是SNAPSHOT版本, 以最大程度保证测试与最终交付产物的一致性;
  3. 在发布阶段, 所有发布组件模块间的相互依赖方式必须是远端依赖, 且是release版本, 以保证发布后产物的pom文件信息正确;

针对这几个期望, 我们采用了通过外部可配置参数的形式, 在编译/发布过程中动态取参决定使用本地或远端依赖的方案.

首先我们在gradle.properties中定义两个参数:

# 是否使用SNAPSHOT依赖
USE_SNAPSHOT=true

# 是否使用本地模块依赖
USE_LOCAL_DEPENDENCY=true

然后我们在buildSrc中定义了一个包含模块依赖信息的类及各模块对应的依赖对象:

/**
 * 包含可发布module信息的封装类
 *
 * @author yi.luo11
 * @date 2023/02/27
 */
open class Library(
    val group: String,
    val artifact: String,
    val version: String,
    val projectPath: String = ":$artifact"
) {

    /**
     * 获取Library对应的[Project]对象
     *
     * @param rootProject 根项目对象
     * @return Library对应的[Project]对象, 可为空
     */
    fun project(rootProject: Project): Project? {
        val root = rootProject.rootProject
        root.subprojects.forEach {
            if (it.name == artifact) return it
        }
        return null
    }

    /**
     * 获取Library是否使用的是SNAPSHOT版本
     *
     * @param rootProject 根项目对象
     * @return 是否使用的是SNAPSHOT版本
     */
    fun useSnapShot(rootProject: Project): Boolean? {
        return project(rootProject)?.useSnapShot()
    }

    /**
     * Library远端依赖地址
     *
     * @param rootProject 根项目对象
     * @return 远端依赖地址
     */
    fun remoteNotation(rootProject: Project): String {
        val useSnapShot = useSnapShot(rootProject) ?: return "abc"
        val version = version + if (useSnapShot) "-SNAPSHOT" else ""
        return "${group}:${artifact}:${version}"
    }
}
...

同时我们定义了几个扩展方法, 用于在各模块中去使用这些依赖对象:

/**
 * 是否使用本地依赖标识扩展方法
 *
 * @return true 使用本地 false 使用远端依赖
 */
fun Project.useLocalDependency(): Boolean {
    return "true".equals(byProperty("USE_LOCAL_DEPENDENCY"), true)
}

/**
 * 是否使用SNAPSHOT版本标识拓展方法
 *
 * @return true 使用SNAPSHOT false 不使用SNAPSHOT
 */
fun Project.useSnapShot(): Boolean {
    return "true".equals(byProperty("USE_SNAPSHOT"), true)
}

/**
 * 获取项目指定参数的值
 */
fun Project.byProperty(str: String): String {
    return findProperty(str) as? String ?: ""
}

/**
 * 动态本地/远端依赖扩展方法
 */
fun Project.libImplementation(library: Library) {

    dependencies.apply {
        val dependency: Any =
            // 本地依赖
            if (useLocalDependency()) {
                project(library.projectPath)
            }
            // 远端依赖
            else {
                library.remoteNotation(project.rootProject)
            }
        add("implementation", dependency)
    }
}

最终我们在项目各模块中引用模块的方式变为:

// ../app/build.gradle.kts

...

dependencies {

    // module dependencies
    libImplementation(`max-ui-core`)

    implementation(official.bundles.common)
    implementation(official.google.flexbox)
    implementation(official.multidex)

    implementation(official.lifecycle.viewModel.ktx)
    implementation(official.lifecycle.liveData.ktx)
    implementation(official.lifecycle.runtime.ktx)

    implementation(official.kotlinx.coroutines.core)
    implementation(official.kotlinx.coroutines.android)

    implementation(core.rv.quickAdapter)
    implementation(core.permission.x)
}

这样我们在编译或发布时, 通过在相应的gradle命令后追加参数就可以控制, 其使用的是本地/远端snapshot/远端release依赖版本.

// 本地依赖编译
./gradlew assemble -PUSE_LOCAL_DEPENDENCY=true

// snapshot依赖发布
./gradlew publish -pUSE_LOCAL_DEPENDENCY=false -pUSE_SNAPSHOT=true

// release依赖发布
./gradlew publish -pUSE_LOCAL_DEPENDENCY=false -pUSE_SNAPSHOT=false

最终, 结合CI/CD, 通过在release分支上配置编译命令, 我们便可以实现之前组件库动态依赖的期望.

相关推荐
HBryce241 分钟前
缓存-基础概念
java·缓存
- 羊羊不超越 -11 分钟前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
一只爱打拳的程序猿15 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧17 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck19 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
白子寰23 分钟前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
王俊山IT35 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。38 分钟前
c++多线程
java·开发语言
小政爱学习!39 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
daqinzl1 小时前
java获取机器ip、mac
java·mac·ip