公共组件库依赖管理
公共组件库项目采用了单project
多module
的模块化开发形式, 在这样的项目结构下, 如何去维护模块及外部依赖是一个我们不能回避的问题. 在组件库阶段一及阶段二的研发过程中, 我们遇到了以下与依赖相关的问题:
- 如何在开发过程中统一各组件模块中的依赖及版本
- 如何高效的解决, 在开发过程中依赖本地组件模块; 测试/发布过程中使用远端依赖的问题
针对问题一,可以采用通用的组件库,从而实现各个模块中外部依赖及版本统一. 当然目前依赖库还在试用阶段, 我们在使用的期间也踩过一些坑, 后面会详细跟大家分享.
针对问题二, 我们通过使用Gradle buildSrc
定义模块动态依赖. 在CI/CD
的基础上, 实现了根据开发/测试/发布阶段自动切换本地或远端依赖的功能.
1. 构建工具简史
Gradle
是我们在安卓开发中最常用的构建工具, 自Android Studio
发布以来, 他一直是默认的构建工具. 但是其实在Gradle
之前, 还有很多知名的构建工具, 例如Java
开发中常用的Maven
等等.
其中最经典的当属Ant
了, Ant
使用的DSL
是xml
, xml
的进化来源于MakeFile
构建的繁琐, 而xml
特点是结构化且好理解, 这比写脚本插件简单多了, 所以迅速流行起来.
随着软件行业的迅速发展, 我们的产品功能越来越多, 业务越来越复杂, 开发团队也日益庞大, 这时候工程管理和工程的标准化问题就开始日益突出, 于是Maven
诞生了. Maven
很好的解决了依赖问题, 引入了标准依赖库对版本进行管理, 并且对工程的目录结构、构建生命周期都做了标准化定义, 极大的方便了工程管理及开发.
但是当Maven
流行一段时间之后, 大家又发现了问题, xml
逻辑简单是不错, 但是写起来太啰嗦, 而且扩展性不够, 此时, Gradle
登场了. Gradle
在Maven
的基础上, 主要解决了两个问题:
- 用一种新的
DSL
, 让语法变的更简洁, 且支持扩展; - 定义了扩展方便且不失标准的构建生命周期;
实际上Gradle
发展至今, 早已超越了上面这两点, 而且还在不断的进化中, 比如buildSrc
的诞生、kts
的支持、KSP
的演进等等.
2. Gradle依赖管理
在Gradle
的日常使用中, 依赖管理其实是非必须的. 但是随着项目的不断发展, 其中的依赖也会越来越多, 这个时候对项目依赖做一个统一的管理很有必要, 我们一般会有以下需求:
- 项目依赖统一管理, 在单独文件中配置
- 不同Module中的依赖版本号统一
- 不同项目中的依赖版本号统一
而随着Gradle
版本迭代, 针对这些需求也有一些方案的更新:
- 最常见的是在全局属性
ext
中定义版本信息, 然后由各module
去使用 - 使用
buildSrc
, 并在buildSrc
中定义版本信息 - 使用
includeBuild
统一配置依赖版本
不过随着Gradle7.0
的推出, 一个新的方式通过使用Catalog
统一依赖版本, 它支持以下特性:
- 对所有
module
可见, 可统一管理所有module
的依赖 - 支持声明依赖
bundles
, 即总是一起使用的依赖可以组合在一起 - 支持版本号与依赖名分离, 可以在多个依赖间共享版本号
- 支持在单独的
libs.versions.toml
文件中配置依赖 - 支持在项目间共享依赖
这也是我们在公共依赖库中使用的方案.
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. 组件库动态依赖实现
不同于常规的应用开发, 组件库最终交付的是可供外部依赖引用的产物. 所以我们期望的是:
- 在开发阶段, 所有针对组件模块的依赖方式可以是本地依赖, 以保证对任意组件模块代码改动是实时生效的;
- 在测试阶段, 提交测试用的Demo应用及组件模块间的相关依赖方式必须是远端依赖, 可以是
SNAPSHOT
版本, 以最大程度保证测试与最终交付产物的一致性; - 在发布阶段, 所有发布组件模块间的相互依赖方式必须是远端依赖, 且是
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
分支上配置编译命令, 我们便可以实现之前组件库动态依赖的期望.