如何共享 Android 不同模块的构建配置

Gradle Convention Plugin

最近想重新梳理学习一遍 Android 的各个知识点,于是新建了一个 AndroidStudy 项目仓库,打算每个知识块新建 1 个 module。
类似这样:

AndroidStudy (Root Project)
├─app (Module0)
├─CustomView (Module1)
├─KotlinCoroutines (Module2)
├─...

然后发现每新建 1 个 Android Library Module 都生成 1 个新的 build.gradle.kts 文件

plugins {
    alias(libs.plugins.com.android.library)
    alias(libs.plugins.org.jetbrains.kotlin.android)
}

android {
    namespace = "com.bqliang.mylibrary"
    compileSdk = 33

    defaultConfig {
        minSdk = 26

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation(libs.core.ktx)
    ...
}

里面的大部分配置都是重复的,像 compileSdk、minSdk、compileOptions、kotlinOptions... 虽然我以后也不会再去改这些配置了,但是代码洁癖还是让我想把这些重复的配置去掉,经过一番搜索,发现 Gradle Convention Plugin 非常适合解决这个问题。

什么是 Convention Plugin

为了解决上面的问题,我们自然很容易想到要把其中可以共享的配置抽取出来,然后在每个 module 中引用这些配置。我们可以写 1 个自定义 AndroidLibraryConventionPlugin 插件,在其中去处理这些共享的构建逻辑,然后在需要的 module 中引用这个插件。这样就可以避免重复的配置了,这样的插件就叫做 Convention Plugin,所以 Convention Plugin 并不是指某个具体的插件(像 com.android.library),而是指一类插件,这类插件的作用就是抽取出一些共享的构建逻辑。

如何编写 Convention Plugin

Gradle 插件有 2 种类型:

  • 二进制插件(Binary Plugin):以编程方式,通过实现 Plugin 接口来编写
  • 脚本插件(Script Plugin):可以简单理解为是构建脚本的拓展,通过在构建脚本中直接编写插件逻辑来编写

插件的源码可以写在下面这几个地方:

  • 构建脚本(Build script)
  • buildSrc project
  • 独立项目(Standalone project)

我们新建 1 个 build-logic 子项目,结构如下,我们将在 AndroidLibraryConventionPlugin 中编写我们的要在 Android Library Module 中共享的构建逻辑。

AndroidStudy (Root Project)
├─...
└─build-logic
   |  settings.gradle.kts
   └─ convention
        |  build.gradle.kts
        └─ src
             └─ main
                  └─ kotlin
                         AndroidLibraryConventionPlugin.kt

build-logic 这种方式就属于独立项目,我们待会会使用复合构建的方式来引用这个项目。

A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included.

复合构建(composite build),简单来说就是一个包含了其他构建(builds)的构建(build)。
普通的 Gradle 多项目/模块构建是在 settings.gradle.kts 中使用 include(...) 来包含其他项目/模块。复合构建和这种方式的区别在于,复合构建使用 includeBuild(...) 来包含其他项目,但包含的不是单一项目,而是完整的构建,换句话来说被包含的项目是完全独立的,可以自己单独进行构建。

AndroidStudy/settings.gradle.kts:

pluginManagement {
    includeBuild("build-logic") // include build-logic module
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "AndroidStudy"
include(":app")
...

因为 included build 的构建是独立的,它不会和 composite build 或者其他的 included builds 共享配置,所以需要在 settings.gradle.kts 中声明依赖仓库源,也要显式声明 Version Catalogs 的文件路径

build-logic/settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
include(":convention")

build-logic/convention/build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

group = "com.bqliang.buildlogic"

// Configure the build-logic plugins to target JDK 17
// This matches the JDK used to build the project, and is not related to what is running on device.
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17.toString()
    }
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}

gradlePlugin {
    // register the convention plugin
    plugins {
        register("androidLibrary") {
            id = "bqliang.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
    }
}

这里的 sourceCompatibility、targetCompatibility、kotlinOptions 只是用来指定编译我们的 Convention Plugin 的 JDK 版本,和共享的构建逻辑没有关系。文件最后注册了我们的 Gradle Convention Plugin,为了后续方便引用,我们可以把 plugin id 写在 libs.versions.toml 里:

[versions]
androidGradlePlugin = "8.1.2"
kotlin = "1.9.10"
...

[libraries]
...
# Dependencies of the included build-logic
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }

[plugins]
# Plugins defined by this project
bqliang-android-library = { id = "bqliang.android.library", version = "unspecified" }
...

我们新建一个 AndroidLibraryConventionPlugin 类,实现 Plugin<> 接口,然后实现 apply 方法,这个方法会在 Gradle 执行时被调用,我们可以在这里编写我们的共享构建逻辑。

build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt:

import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        with(project) {
            with(pluginManager) {
                // Android Library Module 都需要这 2 个插件
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<LibraryExtension> {
                configureKotlinAndroid(this)
                defaultConfig.targetSdk = 34
            }
        }
    }

    private fun Project.configureKotlinAndroid(
        commonExtension: CommonExtension<*, *, *, *, *>,
    ) {
        commonExtension.apply {
            compileSdk = 34

            defaultConfig {
                minSdk = 26
            }

            compileOptions {
                sourceCompatibility = JavaVersion.VERSION_11
                targetCompatibility = JavaVersion.VERSION_11
            }
        }

        configureKotlin()
    }

    private fun Project.configureKotlin() {
        // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947
        tasks.withType<KotlinCompile>().configureEach {
            kotlinOptions {
                // Set JVM target to 11
                jvmTarget = JavaVersion.VERSION_11.toString()
            }
        }
    }
}

我的 Gradle 版本是 8.4,如果上面的代码找不到某些类,可以先尝试升级你的 Gradle 版本
rootProject/gradle/wrapper/gradle-wrapper.properties:

...
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
...

现在我们就可以使用我们的预编译约定插件 AndroidLibraryConventionPlugin 了,回到文章一开始那个 Android Library Module 的 build.gradle.kts,我们可以把里面的大部分配置都去掉了,只是简单的引用一下我们的插件就可以了。

plugins {
    // just simple apply our convention plugin
    alias(libs.plugins.bqliang.android.library)
}

android {
    namespace = "com.bqliang.mylibrary"

    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

dependencies {
    ...
}

当然这里只是把 Android Library Module 的构建逻辑抽取出来了,其实像 Application Module、Jetpack Compose、Room 等等构建逻辑都是可以抽取出来的。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容