阅读 1460

什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin

本文内容来自博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it 英文好的可直接前往

注意科学上网

gradle是如何帮助我们构建的?对于这个问题大所数人可能是这样的

图片摘自上述博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it

.

造成问题的真相

首先要明确的事情:Android-Gradle-Plugin != Gradle

造成上述问题的原因是 Groovy

大多数 Android 开发者并没有在“真正的项目”中使用 groovy ,所以对于我们来说 Android 中的 build.gradle 文件就像是魔术师手中的魔法。

.

救世主:Kotlin & the buildSrc module?

2019年1月23日,Kotlin 1.3.20 发布,并在多平台项目中提供了对Kotlin DSL构建脚本的支持。

构建脚本中可以完成自动补全

我们可以像之前一样写代码,阅读文档,点击进去看到里面的实现,之前的烦恼不再有了

使用 buildSrc 文件夹

关于 builSrc 的职能和使用这里不再赘述,网上文章很多

Gradle dependency management with Kotlin (buildSrc)

Kotlin + buildSrc for Better Gradle Dependency Management

随着时间的推移,我们的 build.gradle 中的代码越来越长

配置 product flavor ,格式化apk文件命名以及路径,动态生成版本号,如果 jenkins 还需要配置一些独立的逻辑那么这个文件会越来越长

我们同一个项目的多个Android Module 大多是公共的样本代码,如果需要更改配置信息,则需要在所有的文件中修改

虽然我们可以使用apply form "..." 将部分逻辑抽取到 xx.gralde 中,可以使用 google 推荐的 ext 配置版本号,targetVersion 等信息。

但是这样真的足够灵活并方便拓展吗?

那么如何使用 kotlin + buildSrc 更改构建文件?

让我们看一下修改之前,这是一个非常庞大的文件,包含近200行代码

点击查看完整代码

.

使用上述方法仅用34行代码即可重构为一段易于理解的代码

.

点击查看完整代码

那么这段新的脚本文件都做了什么?

  1. 依赖的项目的特有插件 com.quickbirdstudios.bluesqaure
  2. 使用了项目自定义的扩展 extension
  3. 定义了module 特有的依赖

.

下面我们来看看究竟是如何实现的吧

步骤1 配置自定义Gralde 插件

  • 在项目根目录创建名为buildSrc文件夹
  • 为该文件夹创建build脚本,buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
    google()
    jcenter()
}

dependencies {
    /* Example Dependency */
    /* Depend on the android gradle plugin, since we want to access it in our plugin */
    implementation("com.android.tools.build:gradle:3.5.3")

    /* Example Dependency */
    /* Depend on the kotlin plugin, since we want to access it in our plugin */
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")

    /* Depend on the default Gradle API's since we want to build a custom plugin */
    implementation(gradleApi())
    implementation(localGroovy())
}
复制代码

现在我们实现我们自己的插件,例如:MyPlugin

.

详情参见官方文档,如何创建Gralde Plugin

.

这里使用 kotlin 语言

.

首先创建 MyPlugin class文件 implements Plugin<Project>

import org.gradle.api.Plugin
import org.gradle.api.Project

open class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
    }
}
复制代码

注意导包不要导错

配置自定义插件的id,例如 com.plugin.test

创建这样的文件 buildSrc/src/main/resources/META-INF/gradle-plugins/com.plugin.test.properties

com.plugin.test.properties 该文件名对应着插件id

在该文件中加入MyPlugin的全路径(包括包名)

implementation-class=com.test.MyPlugin
复制代码

现在我们可以在所有的module中使用该插件了

plugins {
      // ... 
    id("com.android.library")
    id("com.plugin.test")
}
复制代码

这样的处理会使我们插件内部的 apply 函数(方法)得到调用

步骤2 将公共代码抽取到自定义插件中

在我们配置Android 配置文件时我们会有这样的代码

android {
    compileSdkVersion(29)
    // ...
}
复制代码

android{}代码块被称为 扩展(Extension)

这个函数的receiver(不知道如何翻译更合适) 实现了 AppExtension

这里贴一下源码

/**
 * Retrieves the [android][com.android.build.gradle.AppExtension] extension.
 */
val org.gradle.api.Project.`android`: com.android.build.gradle.AppExtension get() =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("android") as com.android.build.gradle.AppExtension

/**
 * Configures the [android][com.android.build.gradle.AppExtension] extension.
 */
fun org.gradle.api.Project.`android`(configure: com.android.build.gradle.AppExtension.() -> Unit): Unit =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)


复制代码

.

我们没必要在所有的module的build.gradle.kts中都配置一次,可以将这段逻辑抽取到一个当我们的插件被应用时的函数中:

// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
    override fun apply(project: Project) {
              project.configureAndroid()
    }
}

// Android.kt
internal fun Project.configureAndroid() = this.extensions.getByType<AppExtension>().run {
        compileSdkVersion(29)
        defaultConfig {
            minSdkVersion(21)
            targetSdkVersion(28)
            versionCode = 2
            versionName = "1.0.1"
            testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
        }

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

            getByName("debug") {
                isTestCoverageEnabled = true
            }
        }

        packagingOptions {
            exclude("META-INF/NOTICE.txt")
          // ...
        }

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

复制代码

.

之后我们创建一个新的Android module 就很简单了,我们只需应用我们的插件com.test.MyPlugin即可自动配置android{}代码块

plugins {
    id("com.android.library")
    id("com.test.MyPlugin")
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
    testImplementation("junit:junit:4.12")
    andriodTestImplementation("com.android.support.test:runner:1.0.2")
    androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}
复制代码

注意:只需使用android{}代码块再次配置即可覆盖插件中预置的配置

·

步骤3 配置默认依赖(dependencies)

一般来说,我们每个android 都需使用下列依赖

  • Kotlin Standard library
  • JUnit
  • Support Test Runner
  • Espresso

我们可以在插件中添加另一个函数 configureDependencies

// MyPlugin.kt
open class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
            project.configureAndroid()
            project.configureDependencies()
    }
}

// Dependencies.kt
const val jUnit = "junit:junit:4.12"
const val androidTestRunner = "com.android.support.test:runner:1.0.2"
const val androidTestRules = "com.android.support.test:rules:1.0.2"
const val mockkAndroid = "io.mockk:mockk-android:1.9"
const val mockk = "io.mockk:mockk:1.9"
const val espressoCore = "com.android.support.test.espresso:espresso-core:3.0.2"

internal fun Project.configureDependencies() = dependencies {
    add("testImplementation", jUnit)

    if (project.containsAndroidPlugin()) {
        add("androidTestImplementation", androidTestRunner)
        add("androidTestImplementation", androidTestRules)
        add("androidTestImplementation", espressoCore)
    }
}

internal fun Project.containsAndroidPlugin(): Boolean {
    return project.plugins.toList().any { plugin -> plugin is AndroidBasePlugin }
}
复制代码

.

现在创建Android module只需

plugins {
    id("com.android.library")
    id("com.test.MyPlugin")
}
复制代码

.

步骤4 配置默认插件

上文的配置有一个缺陷:在Android plugin加载之后我们配置的插件才会正常工作。

但如果我们的所有module都是Android module,我们可以用自定义插件去管理

// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
    override fun apply(project: Project) {
            project.configurePlugins()
            project.configureAndroid()
            project.configureDependencies()
    }
}

//Plugins.kt
internal fun Project.configurePlugins() {
    plugins.apply("com.android.library")
    plugins.apply("org.gradle.maven-publish")
    //其他公共插件
}
复制代码

.

现在创建新的Android module只需

plugins {
    id("com.test.MyPlugin")
}
复制代码

上述配置可以为我们应用 Android Library Plugin 并配置了android代码块和默认依赖

.

如果想创建不加载Android plguin的纯 java module怎么办?

很简单,我们可以多创建几个插件(不要命名为buildSrc)

例如我可以创建 MyBasePlugin, MyAndroidPlugin, MyJavaPlugin, MyMultiplatformPlugin

.

让你的插件变为可配置的

.

我们可以在应用了 Android Library 或者 Android Application 插件的module中使用android{}代码块配置

android {
    compileSdkVersion(29)
}
复制代码

.

Gradle API 将此定义为 Extension

我们也可以定义一些自定义的扩展

.

例如我们想处理两个配置

  1. 这个module发布了吗?
  2. 被发布时packageName是什么?

.

配置好后我们可以这样使用

plugins {
    id("com.plugin.test")
}

test {
    publish = true
    packageName = "my-package"
}
复制代码

.

我们只需创建一个类

open class TestExtension {
    var publish: Boolean = false
    var packageName: String = ""
}
复制代码

.

接下来告诉Gradle有哪些配置

// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
    override fun apply(project: Project) {
          val testExtension: TestExtension = project.extensions.create(
                "test", TestExtension::class.java
            )

            project.configurePlugins()
            project.configureAndroid()
            project.configureDependencies()
    }
}
复制代码

.

原文作者总结了他们公司使用 buildSrc plugin 只用7行代码便实现了以下工作

  • Configure our Android builds
  • Apply default plugins
  • Apply default dependencies
  • Run a custom linter
  • Run aggregated coverage reports
  • Run aggregated test result reports
  • Install default git hooks into the project
  • Setup Multiplatform builds
  • Setup publications for our library modules
  • Deploy libraries to the correct repository (Snapshot/Release)

.

以上为原作者表达的主要信息,原文请移步

.

demo代码

.

英文水平有限,如有纰漏请指教。