• 作者:老汪软件技巧
  • 发表时间:2024-08-25 04:01
  • 浏览量:

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1、版本管理是什么

版本管理(Version Management)是指在开发过程中对项目依赖的各个库、框架、插件等版本进行管理和控制。这个过程确保项目中的所有组件以正确的版本组合在一起运行,以避免兼容性问题、漏洞和其他潜在问题。

对于一个复杂的项目来说,良好的版本管理是至关重要的,它能显著提高项目的可维护性和稳定性。

下面介绍几种常见的依赖版本管理方式。

2、直接指定版本号

新建项目时,在app > build.gradle文件中会有一些官方默认的组件依赖,会在dependencies{ }中声明并直接制定具体的版本号。

代码如下:

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    
    // ...
    
}

这里core-ktx依赖的版本号就是一个具体的版本号1.9.0。

这种方式简单直观,但随着项目的依赖变多时,版本管理分散,升级版本也会比较繁琐,维护成本较高,所以适用于依赖少且变化不频繁的项目。

3、变量占位符

顾名思义,不直接定义具体的版本号,而是通过占位符的方式来代替。

3.1、直接定义

通常来说,很少使用DSL语法在app > build.gradle文件中直接定义变量来用做版本号的,因为跟写死具体版本号也没什么区别了,一般会使用额外扩展属性(ext)或项目全局属性(gradle.properties)。如果是开发SDK,SDK中的依赖版本信息不需要依赖方感知,这么用也可以。

示例:

dependencies {
    val room_version = "2.6.1"
    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")
    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbol Processing (KSP)
    ksp("androidx.room:room-compiler:$room_version")
    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")
    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")
    // optional - RxJava3 support for Room
    implementation("androidx.room:room-rxjava3:$room_version")
    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")
    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:$room_version")
}

这个示例来自于Android Jetpack中的 room 组件库,从依赖声明来看,必选项不止一个,这种情况就可以把版本号抽出来统一管理,即val room_version = "2.6.1"。

3.2、ext

ext全称Extra Properties Extension,是额外扩展属性的意思,以键值对的形式进行存储。

ext主要特性就是其扩展属性都可以通过该扩展的对象进行读写,比如把ext扩展属性写在根目录的build.gradle文件中,此时ext的扩展对象就是rootProject,那么就可以通过rootProject对象对ext中的属性进行读写。

所以我们可以利用ext的扩展特性,在项目的根目录build.gradle文件中来定义依赖的版本号,供所有module来读取,定义在根目录中也有利于属性全局共享和复用。

在根目录build.gradle文件中配置如下:

extra.apply {
    set("core-ktx", "1.9.0")
    set("appcompat", "1.6.1")
}

在app > build.gradle文件中读取:

dependencies {
    implementation("androidx.core:core-ktx:${rootProject.extra.get("core-ktx")}")
    implementation("androidx.appcompat:appcompat:${rootProject.extra.get("appcompat")}")
}

除了依赖的版本号,Android中的一些配置信息也可以使用ext来进行统一管理。

配置代码如下:

extra.apply {
    set("applicationId", "com.yechaoa.gradlex")
    set("minSdk", 23)
    set("targetSdk", 33)
    
    set("core-ktx", "1.9.0")
    set("appcompat", "1.6.1")
}

使用:

android {
    defaultConfig {
        applicationId = rootProject.extra.get("applicationId") as String
        minSdk = rootProject.extra.get("minSdk") as Int
        targetSdk = rootProject.extra.get("targetSdk") as Int
        // ...
    }
}

其他属性以此类推。

这种方式使得版本和参数配置管理更加集中,方便更新,维护性好,缺点是不能直观的看到具体的版本号,需要手动切到根目录的build.gradle文件中查看。

3.3、gradle.properties

gradle.properties是用于定义构建脚本中的配置信息和属性的文件,位于项目的根目录下,以键值对的形式进行存储。

gradle.properties文件中配置的属性所有module都可以读取,也可用于项目版本管理,。

配置代码如下:

# 版本管理示例
applicationId=com.yechaoa.gradlex
minSdk=23
targetSdk=33
core_ktx="1.9.0"
appcompat="1.6.1"

使用:

android {
    defaultConfig {
        applicationId = properties["applicationId"].toString()
        minSdk = properties["minSdk"].toString().toInt()
        targetSdk = properties["targetSdk"].toString().toInt()
        // ...
    }
    dependencies {
        implementation("androidx.core:core-ktx:${properties["core_ktx"]}")
        implementation("androidx.appcompat:appcompat:${properties["appcompat"]}")
    }
}

properties["minSdk"]获取的值是Any对象,需要转为所需要的数据结构。

3.4、小结

这种方式跟ext方式不论是配置上还是使用上,都很相似,优缺点也差不多。版本和参数配置管理更加集中,维护性好,但是不能直观的看到具体的版本号,需要手动切到根目录的gradle.properties文件中查看。

那ext方式和gradle.properties方式怎么选呢?

ext本身比较灵活,可以定义复杂的对象、方法和逻辑,允许嵌套和更丰富的数据结构,还可以根据条件进行动态赋值,但是灵活也会伴随着维护成本的增加。

gradle.properties除了全局属性之外,也可以包含一些构建配置,比如开启并行构建、守护进程,其属性值是静态的,只能定义简单的键值对,不支持复杂的逻辑。

单论版本管理来讲,这二者差别不大,怎么选择就看自己的使用习惯。

4、buildSrc

buildSrc全称Build Sources,它是Gradle提供的一个特殊目录(约定),只能有一个buildSrc目录,且必须位于项目的根目录下,用于子项目之前的构建逻辑共享。

在构建时,Gradle识别到有buildSrc目录就会将其加入到构建流程中,因此buildSrc中定义的类、插件等都可以在项目的构建脚本中使用,不需要额外配置,我们可以理解它是一个专门为构建而生的独立模块。

在buildSrc中,可以自定义Task、插件、脚本函数等,以及依赖版本管理。

下面介绍一下如何使用buildSrc来进行依赖版本管理。

4.1、新建buildSrc文件夹

在project根目录下新建buildSrc目录:

4.2、新建构建脚本

在buildSrc目录下新建构建脚本build.gradle.kts文件:

在buildSrc > build.gradle.kts文件中添加编译buildSrc模块的基础配置。

配置如下:

plugins{
    kotlin("jvm") version "1.7.20"
}
repositories {
    google()
    mavenCentral()
}
dependencies {
    implementation(kotlin("stdlib"))
}

自己按需添加。

4.3、编写版本管理代码

在buildSrc/src/main/kotlin 目录下创建一个Kotlin文件,例如Versions.kt,并在其中定义依赖版本号。

object Versions {
    const val core_ktx = "1.9.0"
    const val appcompat = "1.6.1"
}

4.4、使用

在app > build.gradle.kts文件中引用buildSrc目录下Versions.kt文件中的版本号。

代码如下:

dependencies {
    implementation("androidx.core:core-ktx:${Versions.core_ktx}")
    implementation("androidx.appcompat:appcompat:${Versions.appcompat}")
}

使用buildSrc的方式,在编写是也支持代码提示。

如下图:

此时,我们还可以更进一步,把依赖也用版本管理的方式进行管理。

4.5、进阶

在buildSrc/src/main/kotlin目录下创建一个Kotlin文件,例如Libs.kt,并在其中定义依赖项。

object Libs {
    const val core_ktx = "androidx.core:core-ktx:${Versions.core_ktx}"
    const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
}

依赖中的版本号使用Versions.kt文件中定义的版本号。

然后在app > build.gradle.kts文件中修改依赖方式:

dependencies {
    implementation(Libs.core_ktx)
    implementation(Libs.appcompat)
}

同样支持代码提示,而且支持点击跳转。

动图感受一下:

是不是非常舒服~,除了版本管理,配置相关(targetSdk等)也可以这么使用。

4.6、小结

使用buildSrc进行版本管理,依赖和版本集中在一个模块,结构清晰,提升了代码的可读性和维护性,支持代码提示和跳转,使得开发体验也大大提升,适用于多模块或大型项目,对于小型项目来说可能显得复杂了一些。

5、version catalogs

Version Catalogs全称是version catalog libs,中文是「版本目录」的意思,是Gradle 7.0引入的一项新特性,允许你在一个集中式文件中管理所有依赖版本和插件版本。

version catalogs支持两种使用方式,一种是在settings.gradle(.kts)文件中声明,另一种是在单独的libs.versions.toml文件中声明,这种更解耦一些,推荐使用。

5.1、创建版本目录文件

在根项目的 gradle 文件夹中,创建一个名为 libs.versions.toml的文件。Gradle 默认会在 libs.versions.toml 文件中查找目录,因此建议使用此默认名称,这个叫约定大于协议。

在 libs.versions.toml 文件中,添加以下配置:

[versions]
[libraries]
[plugins]

5.2、定义依赖项和插件

在libs.versions.toml文件中定义依赖项和插件。

代码如下:

[versions]
agp = "8.1.1"
ktx = "1.9.0"
appcompat = "1.6.1"
[libraries]
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

建议为目录中的依赖项块命名采用kebab case,以便更好的代码补全。

kebab case是一种流行的命名规则,它的特点是单词之间使用连字符 (-) 分隔,且所有单词都小写,在web开发中应用比较广泛,比如css属性:font-size。

5.3、修改依赖项和插件

定义好依赖项和插件之后,记得Sync同步一下项目。

然后我们先来修改app > build.gradle文件中依赖项。

代码如下:

dependencies {
    implementation(libs.androidx.ktx)
    implementation(libs.androidx.appcompat)
}

libs为libs.versions.toml文件中的开头名称,androidx.ktx对应[libraries]中的androidx-ktx。

同buildSrc方式一样,也支持代码提示和点击跳转。

再来修改根目录build.gradle文件中的插件。

代码如下:

@Suppress("DSL_SCOPE_VIOLATION")
plugins {
    alias(libs.plugins.android.application) apply false
    // ...
}

如果使用的是低于 8.1 的 Gradle 版本,则需要在使用版本目录时为 plugins{} 代码块添加注解 (@Suppress("DSL_SCOPE_VIOLATION")),原因是Gradle没做Kotlin DSL的扩展适配。

以及修改app > build.gradle文件中的插件引用。

代码如下:

plugins {
    alias(libs.plugins.android.application)
    // ...
}

插件的引用不再是使用id来声明,而是使用alias。

5.4、小结

version catalogs不只是版本管理,还有依赖项和插件的管理,支持模块间共享,集中在一个文件虽然方便维护,但是依赖较多的情况也会libs.versions.toml 文件变得庞大,特别是对于大型项目。

那version catalogs和buildSrc两种版本管理方式应该怎么选呢?

二者有一些相似的地方,比如都支持模块共享,以及代码提示和点击跳转。

对于大型项目来说,个人还是推荐使用buildSrc,不管是依赖管理还是版本管理,可以进行模块化拆分,而不是像version catalogs只有一个libs.versions.toml文件,而且支持构建逻辑共享,比version catalogs更灵活。

version catalogs对于中小型项目来说更友好一些,配置简单,标准化且易读,易于维护。

6、其他

除了上面介绍的几种依赖版本管理方式之外,还有一些其他方式,介绍两个。

6.1、动态添加

通过插件plugin的方式来进行管理,把依赖信息声明在一个配置文件中,类似于一个版本基线的东西,然后在Gradle配置阶段,读取配置中的依赖动态添加到项目中,这种方式多适用于自动化构建的场景,本地开发的话,个人觉得有些增加项目的复杂度了。

示例代码:

class DynamicDependencyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        target.afterEvaluate {
            if (target.hasProperty("dynamicEnable")) {
                target.configurations.all {
                    dependencies.add("implementation", "com.yechaoa.plugin:library:1.0.0")
                }
            }
        }
    }
}

6.2、版本约束

在主项目中利用resolutionStrategy处理机制强制指定依赖的版本,确保一致性,这种虽然手动维护成本较高,但是也能解决版本不一致带来的问题。

示例代码:

configurations.configureEach {
    resolutionStrategy.eachDependency {
        val details = this as DependencyResolveDetails
        val requested = details.requested
        if (requested.group == "com.squareup.okhttp3" && requested.name == "okhttp") {
            details.useVersion("4.10.0")
        }
        // ...
    }
}

7、总结

通过上述几种版本管理方式,你可以根据项目规模、团队协作方式等,来选择适合的依赖版本管理策略,不用局限于某一种,也可以组合来使用。

没有绝对的最好,只有适合的才是最好的。

8、GitHub

/yechaoa/Gra…

9、相关文档