随着移动应用功能的日益复杂和多样化,组件化架构已经成为管理应用代码的一种重要方式。组件化架构的发展使得应用能够以模块化的方式组织和管理,提供更高的灵活性和可扩展性。然而,随之而来的挑战是组件的依赖和版本的管理。在组件化架构下,组件之间存在复杂的依赖关系,而版本管理则需要确保不同组件的兼容性和一致性。本文将探讨组件版本管理的重要性,并介绍一些解决方案和最佳实践,以帮助大家有效地管理组件的版本,确保应用的稳定性和可维护性。
前言
VeSync是一个IoT设备管理的App,随着时间的推移,它不断演变为一个功能复杂的应用程序。除了IoT设备管理功能外,它还包括wellness、商城、社区等其他功能。为了应对这种功能复杂性的增加,VeSync的架构也从最初的单一主工程App逐渐发展为多层级组件化架构。
在架构演变的过程中,组件依赖依然采用 ext 和直接按库引用的依赖方式,在实际开发过程中,引出了下面的问题:
- 代码重复、维护困难、灵活性受限、无法统一依赖(每个组件都直接按库引入)。
- 三方库版本更新无提示,无法追踪 (使用的ext文件管理)。
- 三方库及内部库声明耦合,更新依赖库需要人工审核,实际内部库更新是不需要审核的,这里就产生多余的审核成本。
- 没有限制开发者依赖方式,开发者可以自己在项目直接按库引入。
接下来,我们将逐一介绍Android 项目中常见的依赖管理方案,包括直接按库引入、ext 管理、buildSrc、Composing build 以及 Version Catalogs,同时也会介绍这些方案相应的优势、劣势以及适用场景,看看如何解决上面提到的问题,并帮助开发者更好地理解和选择适合自己项目的依赖管理方案。
一、Android 依赖管理方案详解
1.1 直接按库引入
直接按库引入是一种常见的依赖管理方式,通过在 build.gradle 文件中直接声明依赖库的方式来引入所需的库。
module 1
arduino
// 直接使用
dependencies {
implementation "androidx.appcompat:appcompat:1.3.1"
}
module 2
arduino
// 直接使用
dependencies {
implementation "androidx.appcompat:appcompat:1.3.1"
}
相同的代码重复的写在 gradle 中。一旦appcompat有更新,需要手动修改所有的module,如果忘记修改某个module,这样一个项目里就有多个版本的appcompat。为了解决统一依赖的问题,ext管理出现了。
1.2 ext 管理
ext 管理是一种通过在 build.gradle 文件中定义 ext 变量来管理依赖版本的方式。
ini
//config.gradle
ext {
kotlin_version = '1.8.0'
android = [
compileSdkVersion : 34,
minSdkVersion : 26,
targetSdkVersion : 34,
versionCode : 1,
versionName : "1.0",
]
dependencies = [
// 基本库
kotlin: "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
core_ktx: 'androidx.core:core-ktx:1.3.2',
appcompat: 'androidx.appcompat:appcompat:1.3.1',
material: 'com.google.android.material:material:1.4.0',
constraintlayout: 'androidx.constraintlayout:constraintlayout:2.0.1',
]
}
实际使用:
module 1
csharp
//build.gradle
apply from: 'config.gradle'
...
dependencies {
implementation rootProject.ext.dependencies.kotlin
}
module 2
csharp
//build.gradle
apply from: 'config.gradle'
...
dependencies {
implementation rootProject.ext.dependencies.kotlin
}
这种把依赖和版本统一封装在一个文件中,解决了依赖版本统一的问题,但是依赖是文件的形式,是没有更新提示及代码提示的,也不适用于kts脚本。buildSrc出现了。
1.3 buildSrc
gradle官方文档 给出的解释
当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑,与脚本插件相比,buildSrc 应该是首选,因为它更易于维护、重构和测试代码。
- 在项目的根目录下创建一个名为"buildSrc"的文件夹,名字不要乱改,gradle会特别识别它。
- 创建以下文件
build.gradle.kts
scss
plugins {
`kotlin-dsl`
}
repositories{
mavenCentral()
google()
gradlePluginPortal()
}
Dependencies.kt
ini
// Dependencies.kt
// 不要package!
object BuildVersion {
const val compileSdkVersion = 34
const val minSdkVersion = 26
const val targetSdkVersion = 34
const val versionCode = 1
const val versionName = "1.0"
}
object Versions {
//基本库
const val kotlin = "1.4.21"
const val core_ktx = "1.3.2"
const val appcompat = "1.2.0"
const val material = "1.2.1"
const val constraintlayout = "2.0.1"
}
object VersionConfig {
const val APPCOMPAT= "androidx.appcompat:appcompat:${Versions.appcompat}"
}
实际使用:
module 1
scss
// build.gradle.kts
...
dependencies {
implementation(VersionConfig.kotlin)
}
module 2
scss
// build.gradle.kts
...
dependencies {
implementation(VersionConfig.kotlin)
}
buildSrc解决了ext没有代码提示的问题,但是buildSrc只能用在单个项目中,而且修改了buildSrc中的一个依赖,重新编译是整个项目都需要编译的,这样就影响了编译速度。
1.4 Composing builds
Composing builds 是一种将依赖配置文件拆分成多个独立的 build.gradle 文件的方式。
复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects
- 组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时
- 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作
新建一个项目,或者module ,开发一个插件,
kotlin
// build.gradle.kts
plugins {
id("java-gradle-plugin")
id("org.jetbrains.kotlin.jvm") version "1.8.10"
}
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
//添加Gradle相关的API,否则无法自定义Plugin和Task
implementation(gradleApi())
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
}
gradlePlugin {
plugins {
create("version") {
//添加插件
id = "com.vesync.plugin.version"
//在根目录创建类 VersionPlugin 继承 Plugin<Project>
implementationClass = "com.vesync.plugin.version.VersionPlugin"
}
}
}
// VersionPlugin.kt
class VersionPlugin : Plugin<Project> {
override fun apply(target: Project) {
}
}
// Dependencies.kt
object BuildVersion {
const val compileSdkVersion = 34
const val minSdkVersion = 26
const val targetSdkVersion = 34
const val versionCode = 1
const val versionName = "1.0"
}
object Versions {
//基本库
const val kotlin = "1.4.21"
const val core_ktx = "1.3.2"
const val appcompat = "1.2.0"
const val material = "1.2.1"
const val constraintlayout = "2.0.1"
}
object VersionConfig{
const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
}
实际使用:
scss
//项目 build.gradle.kts 使用
dependencies {
implementation(VersionConfig.kotlin)
}
Composing builds继承了buildSrc所有的优点。当修改了一个依赖版本号,ComposingBuild项目Build的时间几乎比 buildSrc项目快6倍。
1.5 Version Catalogs
Version Catalogs 是一种通过创建版本目录文件来管理依赖版本的方式。
1、启用 version Catalogs
gradle 7.4.1之前:
scss
// gradle 7.4.1之前并不是稳定版本功能,所以需要预先开启功能预览
enableFeaturePreview("VERSION_CATALOGS")
gradle 7.4.1 开始是正式版,默认启用 不需要添加
2、默认方式 gradle目录下新加libs.versions.toml
TOML 文件由4个主要部分组成
[versions] 用于声明可以被依赖项引用的版本
[libraries] 用于声明依赖的别名
[bundles] 用于声明依赖包(依赖组)
[plugins] 用于声明插
ini
// toml 文件
[versions]
agp = '8.1.4'
kotlin-version = '1.8.21'
[libraries]
classpath-tool-build = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
classpath-maven-gradle-plugin = { group = "com.github.dcendents", name = "android-maven-gradle-plugin", version = "2.1" }
classpath-kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin-version" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
javaLibrary = { id = "java-library" }
实际使用:
scss
// build.gradle.kts 使用
alias(libs.plugins.androidApplication)
implementation(libs.lib.kotlin)
// setting.gradle.kts 自定义toml文件存放位置
dependencyResolutionManagement {
......
// 第二种方式使用版本目录
libs {
from(files("./libs.versions.toml"))
}
}
借助 Gradle 版本目录,您能够以可扩容的方式添加和维护依赖项和插件。使用 Gradle 版本目录,您可以在拥有多个模块时更轻松地管理依赖项和插件。您不必对各个 build 文件中的依赖项名称和版本进行硬编码,也不必在每次需要升级依赖项时都更新每个条目,而是可以创建一个包含依赖项的中央版本目录,各种模块可在 Android Studio 协助下以类型安全的方式引用该目录
1.6 方案对比
经过上面的方案介绍,我们从如下几个方面对比一下各个方案的差异。
直接引入 | ext管理 | buildSrc | Composing build | Version Catalogs | |
---|---|---|---|---|---|
统一依赖 | 否 | 是 | 是 | 是 | 是 |
代码提示 | 否 | 否 | 是 | 是 | 是 |
更新提示 | 是 | 否 | 否 | 否 | 是 |
修改依赖编译 | 增量 | 增量 | 全量 | 增量 | 增量 |
依赖组 | 否 | 否 | 否 | 否 | 是 |
语法 | groovykts | groovykts | javakotlin | javakotlin | toml |
学习成本 | 低 | 低 | 较低 | 高 | 较高 |
建议项目类型 | 小型 | 中小型 | 中大型 | 大型模块化组件化 | 大型模块化组件化 |
二、VeSync的组件化依赖管理实践
2.1 方案选择
为了解决我们遇到的问题,结合上面的组件依赖和版本管理方案的调研,最终我们选择使用Version Catalogs方案进行内部库依赖管理,Composing build管理三方库依赖,引入该依赖管理方案后,App架构调整为下图的方式:
选择Version Catalogs及Composing build后,版本管理当中遇到的问题得到了如下的改善:
1)通过配置远程toml文件,解决统一依赖代码重复等问题。
2)Version Catalogs是Google推荐使用依赖方案,具有代码提示、更新提示等优点。
3)Composing build管理三方库依赖,使内部库和三方库的依赖管理分开,更好的审核三方库的引入。
4)针对开发者强制校验开发者按规范引入依赖。
2.2 迁移实现
① 内部库依赖开一个仓库,里面放一个libs.version.toml文件,分支对应项目分支,组件及项目引用改文件。开发一个插件在编译前把toml文件下载到本地,实现一个App 多个组件的依赖统一。
· 之前使用的ext 方案修改为Version Catalogs(通过脚本把各个App ext文件批量转换成toml文件)
ini
// toml 文件
[versions]
agp = '8.1.4'
kotlin-version = '1.8.21'
...
[libraries]
classpath-tool-build = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
classpath-maven-gradle-plugin = { group = "com.github.dcendents", name = "android-maven-gradle-plugin", version = "2.1" }
classpath-kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin-version" }
...
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
javaLibrary = { id = "java-library" }
...
内部库管理流程:
使用:
csharp
// setting.gradle.kts
val tomlVersionUrl = "https://xxxxx.libs.versions.toml"
....下载到gradle目录下
// 也可以下载到指定位置,自定义toml文件存放位置
dependencyResolutionManagement {
......
// 第二种方式使用版本目录
libs {
from(files("./libs.versions.toml"))
}
}
// build.gradle.kts 使用
dependencies {
implementation(libs.vesync.logger)
}
② 三方库依赖开一个项目,开发一个Composing build插件,分支对应项目分支,更新后修改libs.version.toml中的version依赖
· 把ext文件找出三方库,通过脚本转成Composing build文件
kotlin
object BuildVersion {
const val MIN_SDK_VERSION = 24
const val TARGET_SDK_VERSION = 33
const val COMPILE_SDK_VERSION = 33
...
}
object VersionConfig {
...
const val APPCOMPAT = "androidx.appcompat:appcompat:1.6.1"
...
}
class VersionPlugin : Plugin<Project> {
override fun apply(target: Project) {
}
}
三方库管理流程:
使用:
scss
// 项目根build.gradle.kts
// 三方库插件申明
buildscript {
dependencies {
classpath(libs.plugin.version)
}
}
// build.gradle.kts 使用
compileSdk = BuildVersion.COMPILE_SDK_VERSION
dependencies {
implementation(VersionConfig.APPCOMPAT) // VersionConfig为Composing build插件中声明的类
}
③ 开发一个依赖校验插件,检查依赖项只允许使用统一依赖文件的依赖。做到依赖来源统一(实现真正的统一依赖)。
scss
dependencies {
implementation(VersionConfig.APPCOMPAT) // 允许
implementation(libs.lib.appcompat) // 允许
implementation("androidx.appcompat:appcompat:1.6.1") // 不允许,打正式包编译报错
}
三、总结
在 Android 项目中,依赖管理是非常重要的一环。不同的依赖管理方案都有各自的优势和劣势,开发者可以根据项目的实际情况选择适合的方案。
综合考虑项目规模、团队技术水平、依赖库稳定性等因素,选择合适的依赖管理方案对于提高开发效率和项目可维护性至关重要。希望本文对开发者在选择 Android 依赖管理方案时有所帮助。