Skip to content

约定插件深度实践

源:Now in Android | Gradle 官方文档

深入探讨如何构建工业级的约定插件体系,实现全项目构建逻辑的高度内聚与复用。

插件体系架构设计

分层设计理念

推荐的四层架构(参考 Now in Android):

build-logic/convention/
├── AndroidApplicationConventionPlugin.kt      # 应用层
├── AndroidLibraryConventionPlugin.kt         # 库层
├── AndroidFeatureConventionPlugin.kt         # 功能层
├── AndroidComposeConventionPlugin.kt         # Compose 层
├── AndroidHiltConventionPlugin.kt            # Hilt 层
├── AndroidRoomConventionPlugin.kt            # Room 层
└── utils/
    ├── KotlinAndroid.kt                      # 通用配置
    ├── AndroidCompose.kt
    └── ProjectExtensions.kt

层级关系

基础层:配置 Kotlin + Android 基础

核心层:区分 Application / Library

功能层:Compose / Hilt / Room

应用层:Feature 模块组合

插件职责划分

插件类型职责示例
基础插件Kotlin/Java 配置kotlinOptions, jvmTarget
平台插件Android 通用配置compileSdk, minSdk
类型插件Application/LibraryapplicationId, consumerProguardFiles
功能插件特定功能Compose, Hilt, Room

可配置扩展

为什么需要扩展

问题:硬编码的插件不够灵活。

kotlin
// ❌ 硬编码
class ComposePlugin : Plugin<Project> {
    override fun apply(target: Project) {
        // Compose 始终启用
        buildFeatures.compose = true
    }
}

解决方案:提供配置扩展。

kotlin
// ✅ 可配置
class ComposePlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val extension = target.extensions.create<ComposeExtension>("composeConfig")
        
        if (extension.enabled.getOrElse(true)) {
            buildFeatures.compose = true
        }
    }
}

定义扩展

kotlin
// ComposeExtension.kt
interface ComposeExtension {
    val enabled: Property<Boolean>
    val kotlinCompilerVersion: Property<String>
}

使用扩展

kotlin
class AndroidComposeConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            // 创建扩展
            val extension = extensions.create<ComposeExtension>("composeConfig")
            
            extensions.configure<CommonExtension<*, *, *, *, *, *>> {
                buildFeatures {
                    compose = extension.enabled.getOrElse(true)
                }
                
                composeOptions {
                    kotlinCompilerExtensionVersion = extension.kotlinCompilerVersion
                        .getOrElse(libs.findVersion("androidxComposeCompiler").get().requiredVersion)
                }
            }
        }
    }
}

调用方使用

kotlin
// feature/login/build.gradle.kts
plugins {
    id("my.android.compose")
}

composeConfig {
    enabled.set(true)
    kotlinCompilerVersion.set("1.5.10")
}

插件组合模式

基础插件 + 功能插件

设计思路:基础插件提供平台配置,功能插件添加特定能力。

AndroidLibraryConventionPlugin(基础):

kotlin
class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
            }
            
            extensions.configure<LibraryExtension> {
                configureKotlinAndroid(this)
            }
        }
    }
}

AndroidFeatureConventionPlugin(组合):

kotlin
class AndroidFeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            // 应用基础插件
            pluginManager.apply {
                apply("my.android.library")
                apply("my.android.compose")
                apply("my.android.hilt")
            }
            
            // Feature 特定配置
            extensions.configure<LibraryExtension> {
                defaultConfig {
                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                }
            }
            
            // Feature 通用依赖
            dependencies {
                add("implementation", libs.findLibrary("androidx.lifecycle.viewmodel").get())
                add("implementation", libs.findLibrary("androidx.navigation.compose").get())
            }
        }
    }
}

使用

kotlin
// 只需一行
plugins {
    id("my.android.feature")
}

访问 Version Catalog

扩展函数方式

kotlin
// ProjectExtensions.kt
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

internal val Project.libs
    get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")

// 获取版本
internal fun VersionCatalog.version(alias: String): String =
    findVersion(alias).get().requiredVersion

// 获取库
internal fun VersionCatalog.library(alias: String): Provider<MinimalExternalModuleDependency> =
    findLibrary(alias).get()

在插件中使用

kotlin
class AndroidComposeConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            extensions.configure<CommonExtension<*, *, *, *, *, *>> {
                // 使用 libs
                val composeCompilerVersion = libs.version("androidxComposeCompiler")
                
                composeOptions {
                    kotlinCompilerExtensionVersion = composeCompilerVersion
                }
                
                dependencies {
                    val bom = libs.library("androidx-compose-bom")
                    add("implementation", platform(bom))
                }
            }
        }
    }
}

统一依赖管理

在插件中添加依赖

kotlin
class AndroidHiltConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.google.dagger.hilt.android")
                apply("com.google.devtools.ksp")
            }
            
            // 统一添加 Hilt 依赖
            dependencies {
                add("implementation", libs.library("hilt.android"))
                add("ksp", libs.library("hilt.compiler"))
                
                // 测试依赖
                add("testImplementation", libs.library("hilt.android.testing"))
                add("kspTest", libs.library("hilt.compiler"))
            }
        }
    }
}

批量依赖组

kotlin
class TestingConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            dependencies {
                // 单元测试全家桶
                add("testImplementation", libs.library("junit"))
                add("testImplementation", libs.library("kotlin.test"))
                add("testImplementation", libs.library("kotlinx.coroutines.test"))
                add("testImplementation", libs.library("turbine"))
                
                // Android 测试全家桶
                add("androidTestImplementation", libs.library("androidx.test.ext.junit"))
                add("androidTestImplementation", libs.library("androidx.test.espresso.core"))
                add("androidTestImplementation", libs.library("androidx.compose.ui.test"))
            }
        }
    }
}

动态配置

基于 gradle.properties

kotlin
class FeatureFlagPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            extensions.configure<ApplicationExtension> {
                defaultConfig {
                    // 从 gradle.properties 读取
                    val enableLogging = findProperty("feature.logging")?.toString()?.toBoolean() ?: false
                    buildConfigField("Boolean", "ENABLE_LOGGING", "$enableLogging")
                    
                    val apiUrl = findProperty("api.url")?.toString() ?: "https://api.example.com"
                    buildConfigField("String", "API_URL", "\"$apiUrl\"")
                }
            }
        }
    }
}

gradle.properties

properties
feature.logging=true
api.url=https://staging.example.com

基于环境变量

kotlin
class EnvironmentPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            val buildEnv = providers.environmentVariable("BUILD_ENV").getOrElse("dev")
            
            extensions.configure<ApplicationExtension> {
                buildTypes {
                    getByName("release") {
                        // 根据环境选择配置
                        when (buildEnv) {
                            "prod" -> {
                                applicationIdSuffix = null
                                debuggable = false
                            }
                            "staging" -> {
                                applicationIdSuffix = ".staging"
                                debuggable = true
                            }
                            else -> {
                                applicationIdSuffix = ".dev"
                                debuggable = true
                            }
                        }
                    }
                }
            }
        }
    }
}

实战案例

案例1:Now in Android 插件体系

完整的插件列表

kotlin
// build-logic/convention/build.gradle.kts
gradlePlugin {
    plugins {
        register("androidApplicationCompose") {
            id = "nowinandroid.android.application.compose"
            implementationClass = "AndroidApplicationComposeConventionPlugin"
        }
        register("androidApplication") {
            id = "nowinandroid.android.application"
            implementationClass = "AndroidApplicationConventionPlugin"
        }
        register("androidLibraryCompose") {
            id = "nowinandroid.android.library.compose"
            implementationClass = "AndroidLibraryComposeConventionPlugin"
        }
        register("androidLibrary") {
            id = "nowinandroid.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
        register("androidFeature") {
            id = "nowinandroid.android.feature"
            implementationClass = "AndroidFeatureConventionPlugin"
        }
        register("androidHilt") {
            id = "nowinandroid.android.hilt"
            implementationClass = "AndroidHiltConventionPlugin"
        }
        register("androidRoom") {
            id = "nowinandroid.android.room"
            implementationClass = "AndroidRoomConventionPlugin"
        }
    }
}

案例2:多环境配置插件

kotlin
class MultiEnvPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            extensions.configure<ApplicationExtension> {
                flavorDimensions += "environment"
                
                productFlavors {
                    create("dev") {
                        dimension = "environment"
                        applicationIdSuffix = ".dev"
                        versionNameSuffix = "-dev"
                        
                        buildConfigField("String", "API_URL", "\"https://dev-api.example.com\"")
                    }
                    
                    create("staging") {
                        dimension = "environment"
                        applicationIdSuffix = ".staging"
                        versionNameSuffix = "-staging"
                        
                        buildConfigField("String", "API_URL", "\"https://staging-api.example.com\"")
                    }
                    
                    create("prod") {
                        dimension = "environment"
                        
                        buildConfigField("String", "API_URL", "\"https://api.example.com\"")
                    }
                }
            }
        }
    }
}

最佳实践

插件命名规范

<项目名>.<平台>.<类型>[.<功能>]

nowinandroid.android.application
nowinandroid.android.library.compose
nowinandroid.android.feature

文件组织

convention/src/main/kotlin/
├── AndroidApplicationConventionPlugin.kt
├── AndroidLibraryConventionPlugin.kt
├── AndroidFeatureConventionPlugin.kt
├── AndroidComposeConventionPlugin.kt
├── AndroidHiltConventionPlugin.kt
└── utils/
    ├── KotlinAndroid.kt
    ├── AndroidCompose.kt
    └── ProjectExtensions.kt

插件组合

kotlin
// 基础插件
id("my.android.library")

// 功能插件(可选)
id("my.android.compose")
id("my.android.hilt")

// 或使用组合插件
id("my.android.feature")  // 包含上述所有

提取通用配置

kotlin
// 不要在每个插件中重复写
internal fun Project.configureKotlinAndroid(...)
internal fun Project.configureAndroidCompose(...)

使用 Provider API

kotlin
val version = providers.environmentVariable("VERSION").getOrElse("1.0.0")

版本集中管理

kotlin
// 从 Version Catalog 读取
val compileSdk = libs.version("compileSdk").toInt()