以NowInAndroid为范本
build-logic到底是干什么用的
NowInAndroid项目中的 build-logic 是一个专门存放"约定插件"的独立模块 ,它通过插件化的方式,集中管理和复用所有模块(如 app、feature、core)的通用Gradle构建配置。
它的主要目的是将项目中大量重复、分散的Gradle配置代码抽取出来,进行标准化和统一管理。具体来看:
- 消除重复配置 :各个功能模块的
build.gradle.kts文件因此变得非常简洁,通常只需声明应用哪些约定插件和模块特定的依赖,其他通用配置(如Android插件、Compose、Java版本、测试设置等)都封装在插件中。 - 作为"单一可信源" :当需要修改通用构建配置(例如,升级Kotlin版本或调整编译选项)时,只需在
build-logic中修改一次,即可应用到所有相关模块,确保项目配置的一致性并极大降低维护成本
那的确很有必要,
看到一堆的业务模块,如果每个模块都要去重复配置和添加依赖,一旦要修改,又要全部修改一遍,实在是浪费时间。
与app同级的独立文件夹
并不隶属与app,独立的
约定优于配置
在实际的开发中,添加依赖时会想:每增加一个业务模块,就要配置一些相同的依赖,
那么,有没有办法,只需引入一句话就能搞定的呢?
有的,兄弟,包有的。
封装gradle自定义(约定)插件,
每一种插件负责一个特定的配置领域,(Android库,compose,测试等)
职责划分鲜明,组合使用,
避免创建全能的插件,配置组合不够精细,尾大不掉。
当然,
共用的可以约定,
特有的则是按照老办法使用依赖,
这不难理解,
版本目录/gradle/libs.versions.toml
通用的,可以不断收集扩张依赖库
即使是新项目也是拿来就用,
完全不必要,自己重新记录整理,因为这只是一个依赖目录,
具体的使用,还是要在功能模块的gradle中配置。
随着记录,目录会越来越长。
好在,从名称就能清楚知道,是什么依赖库,
如果不清楚的或者升级时需要同步升级的,那就需要备注好。
约定插件
在创建build-logic文件夹的时候,
这是一个不从属于app的独立gradle项目,
而且这实践是Google首次使用,
因此,创建的快捷方式并没有集成到AS中,
所以,文件夹都是手动创建的。(没有忘记怎么用文件夹创建项目)
-
项目目录创建完成
-
开始创建约定插件
-
/convention/src/mian/kotlin
创建一个名为AndroidApplicationConventionPlugin的kotlin类,
约定Android相关的配置;
-
/convention模块下
创建build.gradle.kts
在文件中,配置编译build-logic需要用到的插件和环境,
然后是register注册约定插件,关联上kotlin类,
-
/build-logic目录下
创建setting.gradle.kts
配置项目的根文件,
将convention关联到build-logic项目下,
现在可以了吗?
还不行,
-
关联build-logic到主项目setting.gradle.kts
在主项目的setting.gradle.kts文件里,
pluginManagment的作用域里,
添加includeBuild("build-logic"),
然后,同步sync。
-
使用约定插件
在业务模块build.gradle.kts文件里,
plugins作用域中,alias关联上约定插件,
试试有没有真的能够正常使用。
以上就是创建约定插件的思路。
还是能够看出来,只需要写一次,就可以到处使用,
真的方便不少。
为什么apply()的参数不用libs.findLibrary
kotlin
import com.mircles.allfuntionlearning.libs
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// 1. 应用一些必需的基础插件
pluginManager.apply {
apply("com.android.library") // 应用Android库插件
apply("org.jetbrains.kotlin.android") // 应用Kotlin Android插件
}
// 2. 配置Android闭包(这是约定的核心)
extensions.configure<com.android.build.gradle.LibraryExtension> {
compileSdk = 36 // 统一编译SDK版本
defaultConfig {
minSdk = 24 // 统一最小SDK版本
// 其他通用配置...
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// 如果项目使用Compose,可以在这里统一配置
buildFeatures {
compose = true
}
//??配置的什么
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
// 3. 添加通用依赖
dependencies {
// 添加对所有Android库模块都通用的依赖
//引用的依赖指向libs.versions.toml文件,这是合理的。alias是?
"implementation"(libs.findLibrary("androidx-core-ktx").get())
}
}
}
}
能做到,但没必要。
这里体现的是Gradle插件依赖管理的"两级机制"。
apply()应用的参数是主项目的libs.versions的依赖id。
在编译的时候,build-logic会拉取对应的id和版本,编译到约定插件中。
这个问题非常关键,它触及了Gradle约定插件设计的核心模式。这两种不同的写法,恰恰体现了 "构建配置"与"项目依赖" 在管理哲学上的根本区别。
简单来说:pluginManager.apply 是在"武装"这个约定插件自身,而 dependencies.add 是在为最终应用该插件的业务模块"预装"通用依赖。
用安装操作系统的比喻来理解:
- 插件 (
pluginManager.apply):就像操作系统安装盘自带的驱动程序 。没有它们,安装盘本身就无法识别基础硬件(如Android、Kotlin环境)。这些驱动必须在制作安装盘时就打包进去。 - 库依赖 (
dependencies.add):就像操作系统默认安装的软件包(如记事本、浏览器)。这些软件包的定义和版本信息保存在一个"软件仓库列表"里,安装系统时再从列表里实时下载安装。
下面是这两个层级的详细说明:
第一级:构建"安装盘"(编译 build-logic 项目)
当Gradle编译你的 build-logic 项目,生成约定插件(.class文件)时:
-
pluginManager.apply("com.android.library")- 时机 :在编译
build-logic过程中执行。 - 作用 :告诉Gradle:"在编译我(这个约定插件)的时候,请把Android库插件和Kotlin插件的能力链接进来。" 只有这样,接下来的
android {}和kotlinOptions {}等扩展才能在代码中被识别和调用。 - 结果 :这些插件被编译到约定插件的字节码里,成为其固有的一部分。
- 时机 :在编译
-
libs.findLibrary("kotlin.stdlib").get()- 时机 :这行代码本身 在编译
build-logic时被编译。但findLibrary这个动作并不会发生,它只是一个"查找指令"被保存下来。 - 关键 :
libs对象此时只被用作类型安全的引用 。build-logic项目需要声明对version-catalogAPI 的依赖,才能认识libs这个类型,但并不需要(也不会)在此时解析出具体的kotlin-stdlib:1.9.0。具体的查找动作被推迟到第二级。
- 时机 :这行代码本身 在编译
第二级:使用"安装盘"(业务模块应用约定插件)
当你在 :feature:settings 模块中执行 plugins { id("nowinandroid.android.library") } 时:
-
Gradle会加载并执行已编译好的
AndroidLibraryConventionPlugin的apply方法。 -
当执行到
dependencies.add("implementation", libs.findLibrary("kotlin.stdlib").get())时:- 核心 :这里的
libs对象,不再是build-logic项目里的那个,而是当前业务模块 (:feature:settings)的libs对象! - 查找 :它会在当前模块的上下文中,去查找
libs.versions.toml中定义的kotlin.stdlib,解析出完整的依赖坐标。 - 添加 :将这个解析好的依赖,以
implementation配置添加到当前模块中。
- 核心 :这里的
为什么依赖必须用 findLibrary 这种"推迟"写法?
这是为了获得最大的灵活性与可维护性:
- 单一真实来源 :所有模块的依赖版本始终由根项目的
libs.versions.toml控制。如果你想升级Kotlin版本,只需在toml文件中改一处,所有应用了约定插件的模块都会自动使用新版本。 - 避免耦合 :如果像插件一样在
build-logic中固化依赖版本,那么build-logic就需要为每个库声明具体版本,这会使它变得臃肿,且与业务模块的依赖树紧密耦合。 - 类型安全与IDE支持 :
libs.findLibrary提供了类型安全和IDE自动补全(因为libs对象是从Catalog生成的API)。这比写字符串"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0"要安全、方便得多。
总结与对比
| 特性 | pluginManager.apply("插件ID") |
dependencies.add(libs.findLibrary("xxx")) |
|---|---|---|
| 作用对象 | 装备约定插件本身,使其具有特定能力(配置Android、Kotlin) | 为应用此插件的业务模块添加通用库依赖 |
| 解析时机 | 在编译 build-logic 项目时解析并锁定 |
在业务模块应用插件时,从该模块的Catalog中解析 |
| 版本管理 | 版本在 build-logic/build.gradle.kts 的 dependencies 中锁定 |
版本延迟到业务模块中,从 libs.versions.toml 解析 |
| 类比 | 安装盘自带的驱动(打包时固化) | 从软件库安装的默认软件(安装时根据列表下载) |
所以,这不是不一致,而是一种精妙的分层设计。它确保了约定插件本身的稳定性和业务模块依赖管理的灵活性。
如果你尝试在 build-logic 的插件代码中硬编码一个库的字符串坐标,你会发现业务模块确实能用到这个依赖,但版本升级将变得极其困难 ,因为你必须重新发布 build-logic 插件。而现在的"推迟查找"模式完美地避免了这个问题。