前语

大多数项目经常终究会呈现以下几种情况:

  1. 越来越多的*.gradle(.kts)脚本文件
  2. 非常杂乱的subprojectsandallprojects代码装备块.
  3. 巨大的构建逻辑放在buildSrc目录下
  4. 散落各地并写法眼花缭乱的ext式声明

前两种或许会带来许多麻烦,由于你要么会有许多代码重复,由于开发人员在创立新模块时往往会仿制/张贴这些脚本,要么会有大量的装备块,不必要地将大部分构建逻辑运用到每个模块,在多模块多人开发时经常会呈现这类情况。所以后边呈现了经过buildSrc来统一办理的办法,这种办法在代码仿制方面略微好一些,但 buildSrc 也有问题,由于每次对其中的任何内容进行更改时,它都会使构建缓存失效,有必要再从头构建才干得到新的构建逻辑。在构建逻辑巨大杂乱的时分,这种办法并不高效。也有ext式的kotlin声明依靠,这种办法的声明域不固定,相同命名还会被覆盖,所以多人开发时容易形成零星并不易保护,而且语法标准也放的很开了支持多种格式,可读性上有必定问题。

Gradle为了处理这些痛点,提出了依靠会集声明(VersionCatalog)合作公共约定插件(Convention Plugins)的处理思路。会集声明能够看作是对buildSrcext的合并晋级,条约插件是 Gradle 在子模块之间共享构建逻辑并处理上述问题的办法。

会集声明是经过代码或者读取 *.toml 文件来完成依靠的会集声明。

条约插件确保一切的插件都能够随时增加随意组合,也最大程度的遵从了单一职责思维。运用时模块的构建脚本只用从条约插件中按需选择和装备。

关于会集声明的用法这儿不再重复讨论了,网上相关的运用教程挺多的,咱们能够自行查找检查。

条约插件

借用google 官方的now in android项目来做演示。项目顶层有两个模块build-logicnowinandroid。这儿需求留意的是build-logic项目是作为nowinandroid项意图插件供给模块被复合构建的,这儿需求在nowinandroid项意图settings.gradle.kts里边代码设置。

pluginManagement {
    //传入的参数为build-logic模块的settings.gradle.kts指定的rootProject.name
    //这儿是“build-logic”
    includeBuild("build-logic")
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

再来看一下`build-logic项意图settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    //经过指定的toml文件来创立命名为libs的VersionCatalog 
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}
rootProject.name = "build-logic"
//项目包含子项目convention,也是咱们担任条约插件的模块
include(":convention")

convention的build.gradle.kts要害代码

//此代码块完成了条约插件的注册
gradlePlugin {
    plugins {
        //传入自定义的插件名字,由于运用插件运用的是id所以这个地方运用便利自己了解区分的string就行
        register("androidApplicationCompose") {
            //id字段很要害,咱们在子项目中想运用的时分 传的便是此id
            id = "nowinandroid.android.application.compose"
            //插件的完成类
            implementationClass = "AndroidApplicationComposeConventionPlugin"
        }
        register("androidApplication") {
            id = "nowinandroid.android.application"
            implementationClass = "AndroidApplicationConventionPlugin"
        }
        register("androidApplicationJacoco") {
            id = "nowinandroid.android.application.jacoco"
            implementationClass = "AndroidApplicationJacocoConventionPlugin"
        }
        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("androidLibraryJacoco") {
            id = "nowinandroid.android.library.jacoco"
            implementationClass = "AndroidLibraryJacocoConventionPlugin"
        }
        register("androidTest") {
            id = "nowinandroid.android.test"
            implementationClass = "AndroidTestConventionPlugin"
        }
        register("androidHilt") {
            id = "nowinandroid.android.hilt"
            implementationClass = "AndroidHiltConventionPlugin"
        }
        register("androidRoom") {
            id = "nowinandroid.android.room"
            implementationClass = "AndroidRoomConventionPlugin"
        }
        register("androidFirebase") {
            id = "nowinandroid.android.application.firebase"
            implementationClass = "AndroidApplicationFirebaseConventionPlugin"
        }
        register("androidFlavors") {
            id = "nowinandroid.android.application.flavors"
            implementationClass = "AndroidApplicationFlavorsConventionPlugin"
        }
    }
}

先停一下,咱们来梳理一下上面的内容。

  1. build-logic的settings.gradle.kts装备了versionCatalog
  2. libs.versions.toml里边声明了一切的依靠
  3. convention的build.gradle.kts注册了条约插件,注册的时分指定了id和相应的完成类
  4. 子项目经过选择相应的条约插件的id去装备构建逻辑

OK,那咱们现在来看注册函数参数为androidApplication的条约插件。为什么先选它,原因很简略,咱们知道一切的app模块在构建的脚本里边的plugin下都需求运用com.android.application插件,相应的如果是库模块的话咱们需求运用com.android.library插件,所以咱们选个了解的来剖析。

插件id"nowinandroid.android.application",完成类"AndroidApplicationConventionPlugin",全局搜一下这个id发现两个子项意图build.gradle.kts用到了,分别是appapp-nia-catalog。app-nia-catalog是一个可运行的android app项目,首要展现app项目里边用的的Compose组件,逻辑简略依靠也少许多,咱们就选它来接着剖析,来看下app-nia-catalog的build.gradle.kts代码

plugins {
    //这儿运用了咱们上面说到的条约插件
    id("nowinandroid.android.application")
    id("nowinandroid.android.application.compose")
}
android {
    //默认装备里边只保留了此项目独有的装备,没有指定什么方针版别之类的赋值
    defaultConfig {
        applicationId = "com.google.samples.apps.niacatalog"
        versionCode = 1
        versionName = "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
        // The UI catalog does not depend on content from the app, however, it depends on modules
        // which do, so we must specify a default value for the contentType dimension.
        missingDimensionStrategy(FlavorDimension.contentType.name, NiaFlavor.demo.name)
    }
    packagingOptions {
        resources {
            excludes.add("/META-INF/{AL2.0,LGPL2.1}")
        }
    }
    namespace = "com.google.samples.apps.niacatalog"
    buildTypes {
        val release by getting {
            // To publish on the Play store a private signing key is required, but to allow anyone
            // who clones the code to sign and run the release variant, use the debug signing key.
            // TODO: Abstract the signing configuration to a separate file to avoid hardcoding this.
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}
//经过依靠会集声明完成了2个依靠项
dependencies {
    implementation(project(":core:ui"))
    implementation(project(":core:designsystem"))
    implementation(libs.androidx.activity.compose)
    implementation(libs.accompanist.flowlayout)
}

看到这儿或许脑中现已有了几个疑问

  1. nowinandroid.android.application插件凭什么能够代替com.android.application插件
  2. 为什么android{}和defaultConfig{}块里边能够不必装备最开端说到的那些重复的特点(最低版别、编译版别,kotlin option java version等)

带着这两个疑问,咱们来看一下条约插件的完成类AndroidApplicationConventionPlugin

class AndroidApplicationConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            //要害操作 1
            with(pluginManager) {
                apply("com.android.application")
                apply("org.jetbrains.kotlin.android")
            }
            //要害操作 2
            extensions.configure<ApplicationExtension> {
                configureKotlinAndroid(this)
                defaultConfig.targetSdk = 33
                configureGradleManagedDevices(this)
            }
            //要害操作 3
            extensions.configure<ApplicationAndroidComponentsExtension> {
                configurePrintApksTask(this)
            }
        }
    }
}

很简练吧。作为条约插件自身必定仍是一个Plugin,那么完成Plugin接口,完成apply办法来完成补丁再被运用时需求做的工作。

要害1:target是运用此条约插件的项目对象,再经过with(pluginManaget)函数运用了两个咱们总算了解的插件了com.android.applicationorg.jetbrains.kotlin.android。现在第一个疑问处理了,本来是把本来在build.gralde.kts构建脚本的补丁的运用的操作抽离到这个条约插件了。

要害2: defaultConfig.targetSdk = 33指定了方针sdk版别;点击configureKotlinAndroid(this)函数跳转到KotlinAndroid.kt文件

internal fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension<*, *, *, *>,
) {
    commonExtension.apply {
        compileSdk = 33
        defaultConfig {
            minSdk = 21
        }
        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_11
            targetCompatibility = JavaVersion.VERSION_11
            isCoreLibraryDesugaringEnabled = true
        }
        kotlinOptions {
            // Treat all Kotlin warnings as errors (disabled by default)
            // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
            val warningsAsErrors: String? by project
            allWarningsAsErrors = warningsAsErrors.toBoolean()
            freeCompilerArgs = freeCompilerArgs + listOf(
                "-opt-in=kotlin.RequiresOptIn",
                // Enable experimental coroutines APIs, including Flow
                "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
                "-opt-in=kotlinx.coroutines.FlowPreview",
                "-opt-in=kotlin.Experimental",
            )
            // Set JVM target to 11
            jvmTarget = JavaVersion.VERSION_11.toString()
        }
    }
    //这儿很要害,经过此办法咱们能够拿到versionCatalog,也便是可获取到之前声明的依靠
    val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
    //在条约插件内的函数中增加了依靠
    dependencies {
        add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
    }
}
fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
    (this as ExtensionAware).extensions.configure("kotlinOptions", block)
}

能够看出这儿又是抽离思维。把之前写在build.gradle.kts脚本文件里边的装备抽离出来了例如defaultConfig{},compileOptions{},kotlinOptions{}等。而且能够经过扩展函数的特性,咱们能够拿到project的实力,直接增加依靠,所以这儿咱们能够把一切app类型的模块需求的公共依靠都放在这儿增加。

要害点 3: 眼尖的xdm或许现已留意到了上面extensions.configure<>函数需求传入一个泛型,那么这个泛型咱们哪知道传什么呢?为什么要害2和要害3传的纷歧样呢?这个说实话我也没有找到相关文档,只能反推去看看代码了。先看要害2 的泛型类型ApplicationExtension注释

Extension for the Android Gradle Plugin Application plugin.
This is the android block when the com.android.application plugin is applied.
Only the Android Gradle Plugin should create instances of interfaces in com.android.build.api.dsl.

Android Gradle 插件运用程序插件(com.android.application)的扩展。这是运用 com.android.application 插件时的 android 块。只要Android Gradle插件才干在com.android.build.api.dsl中创立接口实例。

一句话总结这个泛型能够装备之前android运用程序的构建脚本文件下的android{}块下的一切装备

再来看此类型承继的接口CommonExtension的注释

Common extension properties for the Android Application. Library and Dynamic Feature Plugins.
Only the Android Gradle Plugin should create instances of this interface.

Android 运用程序的通用扩展特点。库和动态功用插件。只要Android Gradle插件才干创立此接口的实例。

一句话总结这个泛型一个通用类型,不只能够装备运用程序插件还能装备库和动态功用模块的插件。所以咱们能够经过检查它的子类的命名来判断应该什么情况用什么类型了,例如咱们想在条约插件中装备库类型的项目咱们能够运用LibraryExtension接口

再来看要害 3的泛型类型ApplicationAndroidComponentsExtension注释

Extension for the Android Application Gradle Plugin components. This is the androidComponents block when the com.android.application plugin is applied. Only the Android Gradle Plugin should create instances of interfaces in com.android.build.api.variant.

Android Application Gradle Plugin 组件的扩展。这是运用 com.android.application 插件时的 androidComponents 块。只要Android Gradle插件才干在com.android.build.api.variant中创立接口实例。

一句话总结这个泛型能够装备之前android运用程序的构建脚本文件下的androidComponents{}块下的一切装备

要害3的扩展办法内容也是装备变体相关的,所以这个时分传的类型是ApplicationAndroidComponentsExtension。不了解变体的xdm能够搜gralde多渠道打包或者参考
官网文档来了解。这儿跟本文内容无关,就不做解释了。

以上内容咱们选择了一个相对简略的条约插件的运用来作为比如,是为了便利了解。如果你感兴趣,强烈建议你把其他条约插件也跟着代码过一遍。就如上面说到的,有的插件是为了给库运用的,有的是屏幕适配运用的,有的是给测验运用的,还有是给功用模块运用的。用法和杂乱度纷歧,可是基本上都包括进去了,读完后能对全体运用有个更广更深化的知道。

结论

如果仅仅只用versionCatalog来完成了依靠会集声明,我个人觉得这个跟之前的buildSrc,Kotlin ext声明的办法运用的体会和颠覆性并不是很大,真正吸引更多的开发人员去运用这种构建逻辑的是条约插件合作versionCatalog来抽离构建逻辑和构建逻辑可组合上。

依靠会集声明+条约插件的办法便是完美的吗?必定不是!比方说咱们在开端的时分需求消耗大量的精力和时刻去编写这些插件并测验,在项目没有安稳的时分还需求保护迭代这些插件。可是作为目前google官方演示项目引荐的一种构建办法,里边有许多思路是很有价值和值得学习的。咱们或许不会马上在项目中运用这种办法,可是我个人觉得仍是很有必要去了解的。比方说这儿打破了传统的思维,经过条约插件使得之前一个全体的build.gralde装备变得可拆分可组合,其实我觉得这个思路跟Compose的思维类似,每个条约插件像是每个独立的可组合项,咱们在构建的时分只用把这些可组合项组合起来就行,这样几乎能够精简90%的模版代码也提高了装备的一致性。

参考文章

nia项目代码

Sharing dependency versions between projects (gradle.org)

Sharing build logic between subprojects Sample (gradle.org)

为什么要选择VersionCatalog来做依靠办理? – ()