核心思想
Convention Plugins是一种通过预配置的Gradle插件来标准化和复用构建逻辑的方法,核心理念是:
- 约定优于配置 (Convention over Configuration)
- DRY原则(Don’t Repeat Yourself)
- 构建逻辑集中管理
- 类型安全的配置
典型项目结构
project/
├── build-logic/ # 构建逻辑模块
│ ├── settings.gradle.kts
│ └── convention/ # 约定插件模块
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ ├── AndroidApplicationConventionPlugin.kt
│ ├── AndroidLibraryConventionPlugin.kt
│ ├── AndroidFeatureConventionPlugin.kt
│ └── KotlinLibraryConventionPlugin.kt
├── settings.gradle.kts
├── app/
├── feature/
│ ├── feature-foryou/
│ └── feature-bookmarks/
└── core/
├── core-ui/
└── core-data/
解决的问题
如果不使用Convention Plugins会怎么样,项目中有10个feature模块(比如:feature:foryou,:feature:bookmarks,feature:settings等)。在传统的做法中,每个模块的build.gradle.kts文件都会包含大量相似的配置:
// 在 :feature:foryou/build.gradle.kts 文件中
plugins {
id("com.android.library")
id("kotlin-android")
id("dagger.hilt.android.plugin")
// ...可能还有其他插件
}
android {
// ...大量通用的安卓配置
}
dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:ui"))
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:...")
// ...可能还有10个以上所有feature模块都需要的通用依赖
}
现在,如果你需要为所有feature模块添加一个新的通用依赖库,或者修改一个通用的编译选项,你就必须手动去修改10个不同的build.gradle.kts文件。这不仅繁琐,而且极易出错,非常难以维护。
实现方式
1. 设置build-logic模块
build-logic/settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
dependencyResolutionManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
build-logic/convention/build.gradle.kts
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
}
gradlePlugin {
plugins {
register("androidApplicationCompose") {
id = libs.plugins.nowinandroid.android.application.compose.get().pluginId
implementationClass = "AndroidApplicationComposeConventionPlugin"
}
register("androidApplication") {
id = libs.plugins.nowinandroid.android.application.asProvider().get().pluginId
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidLibraryCompose") {
id = libs.plugins.nowinandroid.android.library.compose.get().pluginId
implementationClass = "AndroidLibraryComposeConventionPlugin"
}
register("androidLibrary") {
id = libs.plugins.nowinandroid.android.library.asProvider().get().pluginId
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidFeature") {
id = libs.plugins.nowinandroid.android.feature.get().pluginId
implementationClass = "AndroidFeatureConventionPlugin"
}
}
}
2. 创建约定插件
AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.android")
apply(plugin = "nowinandroid.android.lint")
apply(plugin = "com.dropbox.dependency-guard")
extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 35
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
configureGradleManagedDevices(this)
}
extensions.configure<ApplicationAndroidComponentsExtension> {
configurePrintApksTask(this)
configureBadgingTasks(extensions.getByType<ApplicationExtension>(), this)
}
}
}
}
AndroidApplicationComposeConventionPlugin.kt
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension)
}
}
}
AndroidLibraryComposeConventionPlugin.kt
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(extension)
}
}
}
AndroidLibraryConventionPlugin.kt
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.android")
apply(plugin = "nowinandroid.android.lint")
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
testOptions.targetSdk = 35
lint.targetSdk = 35
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true
configureFlavors(this)
configureGradleManagedDevices(this)
// The resource prefix is derived from the module name,
// so resources inside ":core:module1" must be prefixed with "core_module1_"
resourcePrefix =
path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_")
.lowercase() + "_"
}
extensions.configure<LibraryAndroidComponentsExtension> {
configurePrintApksTask(this)
disableUnnecessaryAndroidTests(target)
}
dependencies {
"androidTestImplementation"(libs.findLibrary("kotlin.test").get())
"testImplementation"(libs.findLibrary("kotlin.test").get())
"implementation"(libs.findLibrary("androidx.tracing.ktx").get())
}
}
}
}
AndroidFeatureConvention.kt
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "nowinandroid.android.library")
apply(plugin = "nowinandroid.hilt")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
extensions.configure<LibraryExtension> {
testOptions.animationsDisabled = true
configureGradleManagedDevices(this)
}
dependencies {
"implementation"(project(":core:ui"))
"implementation"(project(":core:designsystem"))
"implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get())
"implementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
"implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
"implementation"(libs.findLibrary("androidx.navigation.compose").get())
"implementation"(libs.findLibrary("androidx.tracing.ktx").get())
"implementation"(libs.findLibrary("kotlinx.serialization.json").get())
"testImplementation"(libs.findLibrary("androidx.navigation.testing").get())
"androidTestImplementation"(
libs.findLibrary("androidx.lifecycle.runtimeTesting").get(),
)
}
}
}
}
3. 核心方法:apply
当你将这个插件引用到一个模块时(例如,在:feature:foryou中应用它),Gradle就会执行这个插件类中的apply方法。逐行分析这个方法做了什么:
// 在 AndroidFeatureConventionPlugin.kt 中
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { // 'target' 就是应用这个插件的模块, 比如 :feature:foryou
with(target) {
// --- 动作一:应用其他更基础的约定插件 ---
// 这行代码在说:“一个 `feature` 模块,首先它是一个 `library` 模块,
// 并且它也需要 Hilt 依赖注入和序列化功能。”
// 这体现了插件的组合能力。
apply(plugin = "nowinandroid.android.library")
apply(plugin = "nowinandroid.hilt")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
// --- 动作二:配置通用的 Android 属性 ---
// 这里统一为所有 feature 模块配置 'android' 代码块。
// 比如,统一关闭测试中的动画,统一配置测试设备。
extensions.configure<LibraryExtension> {
testOptions.animationsDisabled = true
configureGradleManagedDevices(this)
}
// --- 动作三:添加所有 feature 模块都需要的通用依赖 ---
// 这是最强大的部分。它声明了“每一个” feature 模块都会自动获得这些依赖。
// 无需在各自的 build.gradle.kts 中重复添加。
dependencies {
"implementation"(project(":core:ui"))
"implementation"(project(":core:designsystem"))
"implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get())
"implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
// ... 以及其他所有通用依赖
}
}
}
}
4. 共享配置函数
KotlinAndroid.kt
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = 34
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
configureKotlin()
}
private fun Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
}
}
}
5. 在根项目中引入
settings.gradle.kts
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
6. 在模块中引入
app/build.gradle.kts
plugins {
id("myapp.android.application")
id("myapp.android.application.compose")
}
android {
namespace = "com.example.myapp"
// 只需配置特定于此模块的内容
defaultConfig {
applicationId = "com.example.myapp"
}
}
dependencies {
implementation(project(":feature:home"))
implementation(project(":feature:profile"))
}
feature/feature-home/build.gradle.kts
plugins {
id("myapp.android.feature")
}
android {
namespace = "com.example.myapp.feature.home"
}
dependencies {
// Feature 特定的依赖
}
Convention Plugin的优势
- DRY (Don’t Repeat Yourself) 原则:消除了在几十个文件中复制粘贴的构建逻辑。
- 单一事实来源 (Single Source of Truth):如果需要更新一个所有
feature模块都在用的库版本(比如 Lifecycle),你只需要在AndroidFeatureConventionPlugin.kt这一个文件里修改即可。 - 简洁与清晰:每个模块的构建脚本变得非常短,只包含对该模块独一无二的配置,可读性大大提高。
- 类型安全与 IDE 支持:因为这些插件是用 Kotlin 编写的,你可以享受到 IDE 的自动补全、编译时类型检查和方便的导航跳转,这比传统的 Groovy 脚本要强大得多。
这些都是now-in-android所应用的,更详细的可以通过此项目学习