Gradle 提供了 Composite Builds 的多项目构建方式,也就是通过 build-logic 目录单独存放项目的构建逻辑,适合在构建之间共享逻辑或者独立访问共享构建逻辑

  • 该目录可以在项目的根目录下,也可以在根目录外

  • 该目录有自己的 settings.gradle.ktsbuild.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.ktsgradle.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) {
        
    }
}

相关的构建有哪些?

  1. 声明 com.android.applicationorg.jetbrains.kotlin.android 插件

  2. 配置 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())
            }
        }
    }
}