一、为什么需要约定插件?
说实话,我刚带团队那会儿,最头疼的就是各个模块的Gradle脚本写得五花八门。有人用implementation,有人用api;有人开混淆,有人不开;测试配置更是各写各的。每次Code Review都得盯着build.gradle看半天。
后来我意识到,这其实是个「标准化」的问题。你想想看,一个项目十几个模块,如果每个模块都自己写一遍配置,不仅累,还容易出错。更麻烦的是——哪天你想升级AGP版本,得挨个改十几个文件,漏一个就等着编译报错吧。
Gradle Convention Plugin就是来解决这个问题的。说白了,它就是一个「配置模板」。你把公共的配置抽出来,写成一个插件,然后各个模块apply一下就行了。我习惯叫它「约定插件」,因为它本质上是在建立团队的构建约定。
二、Convention Plugin 到底是什么?
从技术上讲,Convention Plugin就是一个Gradle插件。但它不是用来做编译或者打包这种底层事情的,它的职责是「配置」——给项目设置默认值、统一行为。
举个例子:
// 传统的做法:每个模块都写一遍
android {
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
}
}
// 用了Convention Plugin之后:模块里只需要写
plugins {
id 'com.example.android-conventions'
}
嗯,差别很明显吧?模块的build.gradle从几十行变成了几行。而且,所有模块的compileSdk、minSdk这些值,都由插件统一管理。改一处,全局生效。
三、如何创建一个 Convention Plugin?
创建过程其实不复杂,我带你走一遍。我个人习惯在项目根目录下建一个 build-logic 目录,专门放这些约定插件。
3.1 目录结构
project-root/
├── build-logic/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ └── com/example/
│ ├── AndroidApplicationConventionPlugin.kt
│ ├── AndroidLibraryConventionPlugin.kt
│ └── KotlinConventionPlugin.kt
├── app/
├── feature/
└── settings.gradle.kts
3.2 配置 build-logic 模块
首先,在 build-logic/build.gradle.kts 里声明依赖:
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation("com.android.tools.build:gradle:8.2.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
}
然后在 build-logic/settings.gradle.kts 里把它注册为一个独立的构建:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "build-logic"
3.3 编写第一个约定插件
接下来,写一个Android应用模块的约定插件。我习惯用Kotlin来写,因为Gradle Kotlin DSL本身就是Kotlin,写起来很顺畅。
// AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins.apply("com.android.application")
plugins.apply("org.jetbrains.kotlin.android")
extensions.configure<ApplicationExtension>("android") {
compileSdk = 34
defaultConfig {
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
}
}
}
}
写完之后,在 build-logic/src/main/resources/META-INF/gradle-plugins/ 下创建一个属性文件,把插件ID和实现类映射起来:
# com.example.android-application.properties
implementation-class=com.example.AndroidApplicationConventionPlugin
3.4 在项目中使用
最后,在根项目的 settings.gradle.kts 里include这个build-logic:
pluginManagement {
includeBuild("build-logic")
}
然后在app模块的build.gradle.kts里:
plugins {
id("com.example.android-application")
}
搞定。就这么简单。
四、统一 Android 模块配置
实际项目中,我们通常有application和library两种模块。它们的配置大部分相同,只有少数地方不一样。比如application需要applicationId,library不需要。
我一般会写两个插件:一个给app用,一个给library用。但公共部分会抽出来共享。
// 公共配置,放在一个单独的类里
object AndroidConfig {
const val compileSdk = 34
const val minSdk = 21
const val targetSdk = 34
}
// 在插件里引用
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins.apply("com.android.library")
plugins.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension>("android") {
compileSdk = AndroidConfig.compileSdk
defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
}
// ... 其他公共配置
}
}
}
}
这样做的好处是,哪天你想把minSdk从21升到23,只需要改AndroidConfig这一个地方。我曾经在一个老项目里,因为minSdk分散在十几个模块里,升级的时候改漏了一个,结果低版本手机直接Crash。从那以后,我就再也不敢把配置散着写了。
五、统一 Kotlin 配置
Kotlin的配置其实也有不少坑。比如JVM目标版本、编译器参数、协程的编译优化等等。如果不统一,很容易出现「我这个模块用1.9,那个模块用1.8」的混乱局面。
我习惯写一个专门的Kotlin约定插件:
class KotlinConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins.apply("org.jetbrains.kotlin.android")
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xjvm-default=all"
)
}
}
}
}
}
然后,Android的约定插件里,直接apply这个Kotlin插件就行:
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins.apply("com.example.kotlin-conventions")
plugins.apply("com.android.application")
// ... 其他配置
}
}
}
这样,Kotlin的版本和参数就统一了。你想想看,如果每个模块都自己写一遍 jvmTarget = "17",万一有人写成16,编译的时候不会报错,但运行起来就可能出问题。这种隐形的坑,用约定插件就能轻松避免。
六、统一测试配置
测试配置是另一个容易乱的地方。有人用JUnit4,有人用JUnit5;有人用MockK,有人用Mockito。更别说测试覆盖率、测试报告这些了。
我一般会写一个测试约定插件:
class TestConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
dependencies {
add("testImplementation", "junit:junit:4.13.2")
add("testImplementation", "org.mockito:mockito-core:5.6.0")
add("androidTestImplementation", "androidx.test.ext:junit:1.1.5")
add("androidTestImplementation", "androidx.test.espresso:espresso-core:3.5.1")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
}
}
}
然后在Android约定插件里,把测试插件也apply进来。这样,所有模块的测试框架和版本就统一了。
Libs.kt 文件,把所有依赖版本集中定义。这样升级依赖的时候,一目了然。
七、知识体系总览
说了这么多,我画了一张图帮你梳理一下整个知识结构。你可以看到,约定插件的核心就是「封装」和「复用」,把散落在各个模块的配置收拢到一处。
八、写在最后
约定插件这个东西,说白了就是「把配置当代码来管理」。你把它当成一个独立的模块去设计、去测试、去版本控制,而不是把配置散落在各个build.gradle里。
我刚开始用的时候,也觉得多此一举——「不就是几行配置吗,复制粘贴不就行了?」但项目一大了,模块一多,你就知道统一管理的好处了。尤其是团队协作的时候,新人来了,直接apply插件就行,不用再教他「compileSdk要写多少、minSdk是多少」这些琐碎的事情。
嗯,这就是约定插件的核心思路。下一节我们会深入讲讲如何编写更复杂的插件,包括如何接收外部参数、如何做条件判断等等。但今天的内容,已经足够你开始动手改造自己的项目了。
公众号:蓝海资料掘金营,微信deep3321