Gradle 提供了 Composite Builds 的多项目构建方式,也就是通过 build-logic 目录单独存放项目的构建逻辑,适合在构建之间共享逻辑或者独立访问共享构建逻辑
该目录可以在项目的根目录下,也可以在根目录外
该目录有自己的
settings.gradle.kts和build.gradle.kts
Now In Android 中使用这种构建方式(下面的 Nia 指代 Now In Android)
将构建逻辑插件化
在 Nia 中 build-logic 主要是将一些重复的构建逻辑提取出来,使用 Gradle 提供的插件能力将这些逻辑简洁运用到各个子模块;
子模块当需要某种构建逻辑时,只需要使用该插件即可,而无需反复编写这些构建逻辑;
创建build-logic
在 AS 中创建一个新的文件夹,在内部声明一个 settings.gradle.kts,用于配置该构建逻辑的一些信息:
配置
libs:使用libs.versions.toml管理的依赖,在build-logic中无法访问到,因此需要手动创建一个libs来表示项目管理的依赖其他配置和正常的项目差不多,比如说声明项目的名称、包含了哪些子项目等等;
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
// 根据项目所在的位置
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
build-logic相当于一个独立的项目,里面的settings.gradle.kts和gradle.properties都是单独针对build-logic的配置;
在创建了 settings.gradle.kts 文件后,AS 还未能将其识别为一个项目,执行 sync now 后没有变化;
还需要在项目(非 build-logic)的 settings.gradle.kts 中声明包含该目录;
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}获取依赖库提供的 Gradle Plugin
大部分 Goodle 的库都提供了对应的 Gradle Plugin,需要在 libs.versions.toml 文件中声明这些插件;
为什么要获取这些插件,因为需要要在代码中获取到对应的插件实例,并进行配置;
下面是一些部分提供的插件:
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" }在 build-logic 创建子项目
在 build-logic 可以创建多个子项目,可以用于归类不同的构建逻辑;
这里在 convention 中将可以抽离出来的构建逻辑写到插件中;
在 convention 子项目中的 build.gradle.kts 用于声明依赖的插件并声明插件:
首先要应用 kotlin-dsl 插件,才能使用 implement、compileOnly 等方法;
plugins {
`kotlin-dsl`
}然后依赖构建所需要的 Gradle Plugin
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.android.tools.common)
compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.room.gradlePlugin)
}以及配置一些构建的信息
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}获取版本管理
因为要拿到 libs.versions.toml 文件中声明的库,所以要先拿到对应的实例对象 VersionCatalog,也就是上面已经声明的 libs
versionCatalogs {
create("libs") {
// 根据项目所在的位置
from(files("../gradle/libs.versions.toml"))
}
}使用扩展属性创建一个对应实例:
val Project.libs
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs") 编写插件
将某些重复共享的构建逻辑抽离出来,单独写成一个 Gradle Plugin
写 Gradle Plugin 只需要实现 org.gradle.api.Plugin 接口,同时插件是应用在项目上的,因此还需要有 org.gradle.api.Project
需要实现 Plugin 接口中的
apply方法, 传递过来的target是该插件被应用到的项目实例;
下面以将 Android Application 类型模块的逻辑抽离为例:
class AndroidApplicationConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
}
}相关的构建有哪些?
声明
com.android.application和org.jetbrains.kotlin.android插件配置
android闭包块
// 声明所需插件
with(target) {
with(pluginManager) {
// 使用插件,如果已经应用则不会执行
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
}
}配置 android 闭包块,这个是由 com.android.build.api.dsl.ApplicationExtension 提供的,
// 配置 android 闭包块
with(target) {
extensions.configure<ApplicationExtension> {
// ....
}
}往往 Android Application 模块和 Android Library 模块的 android 配置信息大致相同,因此可以将这些共有的配置信息单独抽离出一个函数:
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*,*,*,*,*,*>
) {
commonExtension.apply {
compileSdk = 34
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
}
configureKotlin()
dependencies {
// 同时可以添加这些模块所需要的库,根据自己的实际情况
}
}
// 配置项目的 Kotlin
private fun Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
// 协程实验性功能
val warningsAsErrors: String? by project
allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs = freeCompilerArgs + listOf(
// 启用协程的湿陷性功能
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
}
}
}注册插件
写好插件后,还需要注册插件,才能让别的项目使用这个插件
在 build.gradle.kts 中注册插件:
id是这个插件的唯一标识符
implementationClass 是这个插件的实现类,也就是上面我们写好的插件(注意是否有包前缀)
// 注册插件
gradlePlugin {
plugins {
register("androidApplication") {
id = "li.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
}
}可以开启插件的验证功能:
tasks {
validatePlugins {
enableStricterValidation = true
failOnWarning = true
}
}随后 sync now 后就能使用这些注册的插件了
使用插件
注册完插件后,我们需要如何使用这些插件呢?
在 libs.versions.toml 文件中声明已经注册的插件:
[plugins]
li-android-application = { id = "li.android.application", version = "unspecified" }我们在根项目的
settings.gradle.kts文件的pluginManagement中声明了插件的来源就有build-logic,因此查找插件时就会去build-logic去查找;
在 libs.versions.toml 文件中声明好后(注意 sync now),就能在项目的 build.gradle.kts 文件中使用到这些插件了。
比如 app 模块中:
plugins {
alias(libs.plugins.li.android.application)
}对于插件中已经声明好的配置信息,就无需继续声明,只需要单独配置该模块 特有 的信息即可;
思考
Nia 是一个组件化的App,它按照功能划分了多个模块,如果每个组件都需要单独配置一份逻辑,那么这种冗余的工作是枯燥的,需要cv,还需要检查有没有错误;同时当某些相同的配置需要修改时,还需要去到每个子模块统一修改,这就更难受了;
将这些逻辑抽离出来并插件化,每个模块只需要简单的应用下插件即可,只需要关心该模块特定的配置信息;
而且当配置信息修改时,只需要修改插件中的构建逻辑即可,只需要在 build-logic 单点修改;
其他插件逻辑
Library模块相关
class AndroidLibraryConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("li.android.lint")
}
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
testOptions.animationsDisabled = true
// 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() + "_"
}
dependencies {
add("testImplementation", kotlin("test"))
add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
}
}
}
}Compose相关
配置逻辑
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
// 启用 compose Feature
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
}
dependencies {
val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", libs.findLibrary("androidx-compose-material").get())
add("implementation", libs.findLibrary("androidx-compose-material3").get())
add("implementation", platform(bom))
add("androidTestImplementation", platform(bom))
add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get())
add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get())
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs += buildComposeMetricsParameters()
freeCompilerArgs += stabilityConfiguration()
freeCompilerArgs += strongSkippingConfiguration()
}
}
}
/**
* 配置 Compose Compiler Reports,输出可组合函数的稳定情况,用于调试
* */
private fun Project.buildComposeMetricsParameters(): List<String> {
val metricParameters = mutableListOf<String>()
val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics")
val relativePath = projectDir.relativeTo(rootDir)
val buildDir = layout.buildDirectory.get().asFile
val enableMetrics = (enableMetricsProvider.orNull == "true")
if (enableMetrics) {
// <buildDir>/compose-metrics/
val metricsFolder = buildDir.resolve("compose-metrics").resolve(relativePath)
metricParameters.add("-P")
metricParameters.add(
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath,
)
}
val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports")
val enableReports = (enableReportsProvider.orNull == "true")
if (enableReports) {
val reportsFolder = buildDir.resolve("compose-reports").resolve(relativePath)
metricParameters.add("-P")
metricParameters.add(
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath
)
}
return metricParameters.toList()
}
private fun Project.stabilityConfiguration() = listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=${project.rootDir.absolutePath}/compose_compiler_config.conf",
)
private fun Project.strongSkippingConfiguration() = listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
)
Application模块使用
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// 应用 application 插件
pluginManager.apply("com.android.application")
val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension)
}
}
} Library模块使用
class AndroidLibraryComposeConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(extension)
}
}
}Hilt相关
class AndroidHiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.google.devtools.ksp")
apply("dagger.hilt.android.plugin")
}
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.compiler").get())
}
}
}
}Room相关
class AndroidRoomConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply {
apply("androidx.room")
apply("com.google.devtools.ksp")
}
extensions.configure<KspExtension> {
arg("room.generateKotlin", "true")
}
extensions.configure<RoomExtension> {
// The schemas directory contains a schema file for each version of the Room database.
// This is required to enable Room auto migrations.
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
schemaDirectory("$projectDir/schemas")
}
dependencies {
add("implementation", libs.findLibrary("room.runtime").get())
add("implementation", libs.findLibrary("room.ktx").get())
add("ksp", libs.findLibrary("room.compiler").get())
}
}
}
}Lint相关
class AndroidLintConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
when {
pluginManager.hasPlugin("com.android.application") ->
configure<ApplicationExtension> { lint(Lint::configure) }
pluginManager.hasPlugin("com.android.library") ->
configure<LibraryExtension> { lint(Lint::configure) }
else -> {
pluginManager.apply("com.android.lint")
configure<Lint>(Lint::configure)
}
}
}
}
}
private fun Lint.configure() {
xmlReport = true
checkDependencies = true
}组件化相关
对于一些基础组件,单独放到 core:xxx 模块中,而业务逻辑放到 feature:xxx 模块
对于 feature 组件需要都要依赖的 core 组件,就可以在插件中统一依赖
class AndroidFeatureConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply {
apply("li.android.library")
apply("li.android.hilt")
}
extensions.configure<LibraryExtension> {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true
}
}
dependencies {
add("api", project(":core:ui"))
add("api", project(":core:model"))
add("api", project(":core:data"))
add("api", project(":core:common"))
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
}
}
}
}