I tutors Gradle! Kotlin and the buildSrc Plugin made me Love it

Pay attention to scientific Internet

How does Gradle help us build? Most people probably do

Picture taken from the above blog I tutors Gradle! Kotlin and the buildSrc Plugin made me love it

.

The truth that caused the problem

First things first: Android-gradle-plugin! = Gradle

The reason for the above problems is Groovy

Most Android developers don’t use Groovy in “real projects,” so for us the Build. gradle file in Android feels like magic in the hands of a magician.

.

Savior: Kotlin & the buildSrc Module?

On January 23, 2019, Kotlin 1.3.20 was released and provides support for Kotlin DSL build scripts in multi-platform projects.

Autocompletion can be done in the build script

We can write the code as before, read the documentation, click in and see the implementation, and no longer worry about it

Use the buildSrc folder

The function and use of builSrc will not be described here, there are many articles on the web

Gradle dependency management with Kotlin (buildSrc)

Kotlin + buildSrc for Better Gradle Dependency Management

As time goes on, the code in our build.gradle gets longer and longer

Configure product flavor, format apK file name and path, dynamically generate version number. If Jenkins needs to configure some independent logic, the file will get longer and longer

Our multiple Android Modules on the same project are mostly common sample code, and if configuration information needs to be changed, it needs to be changed in all the files

Although we can use apply form “…” To extract some of the logic into xx.gralde, you can use Google recommended Ext to configure the version number, targetVersion and other information.

But is it really flexible enough to expand?

So how do you use Kotlin + buildSrc to change the build file?

Let’s take a look before the changes. This is a very large file, containing nearly 200 lines of code

Click here to see the full code

.

Using the above method, you can refactor into an easy-to-understand piece of code in just 34 lines

.

Click here to see the full code

So what does this new script file do?

  1. Dependent project-specific plug-inscom.quickbirdstudios.bluesqaure
  2. Project custom extensions are usedextension
  3. Defines module-specific dependencies

.

Let’s see how it works

Procedure Step 1 Configure the customized Gralde plug-in

  • Create a name in the project root directorybuildSrcfolder
  • Create a build script for this folder,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())
}
Copy the code

Now let’s implement our own plugin, for example: MyPlugin

.

For details, see the official documentation. How to create the Gralde Plugin

.

Kotlin language is used here

.

MyPlugin class file implements Plugin

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

open class MyPlugin : Plugin<Project> {
    override fun apply(project: Project){}}Copy the code

Be careful not to misguide the package

Configure the id of a custom plug-in, for example, com.plugin.test

To create this file buildSrc/SRC/main/resources/meta-inf/gradle – plugins/com. The plugin. The test. The properties

Com. The plugin. The test. The properties of the file name corresponds to the plug-in id

Add the full path of MyPlugin (including package name) to the file

implementation-class=com.test.MyPlugin
Copy the code

We can now use the plug-in in all modules

plugins {
      // ... 
    id("com.android.library")
    id("com.plugin.test")}Copy the code

This processing causes the apply function (method) inside our plug-in to be called

Step 2 Extract common code to a custom plug-in

We’ll have this code when we configure the Android profile

android {
    compileSdkVersion(29)
    // ...
}
Copy the code

Android {} code blocks are called extensions.

Does this function receiver implement AppExtension

Here is the source code

/** * 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)


Copy the code

.

We don’t need to configure all module build.gradle. KTS at once, we can extract this logic into a function when our plugin is applied:

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

// Android.kt
internal fun Project.configureAndroid(a) = 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
        }
}

Copy the code

.

It’s easy to create a new Android Module by applying our plugin com.test.myplugin to automatically configure the Android {} code block

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." ")}Copy the code

Note: Simply reconfigure using the Android {} code block to override the preset configuration in the plug-in

,

Step 3 Configure default dependencies.

In general, we use the following dependencies for each Android

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

We can add another function, configureDependencies, to the plug-in

// 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(a) = dependencies {
    add("testImplementation", jUnit)

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

internal fun Project.containsAndroidPlugin(a): Boolean {
    return project.plugins.toList().any { plugin -> plugin is AndroidBasePlugin }
}
Copy the code

.

Creating an Android Module now only requires

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

.

Step 4 Configure the default plug-in

There is a drawback to the above configuration: our configured plug-in will not work until the Android plugin is loaded.

But if all of our modules are Android modules, we can use custom plug-ins to manage them

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

//Plugins.kt
internal fun Project.configurePlugins(a) {
    plugins.apply("com.android.library")
    plugins.apply("org.gradle.maven-publish")
    // Other public plug-ins
}
Copy the code

.

Now creating a new Android Module is just a matter of

plugins {
    id("com.test.MyPlugin")}Copy the code

The above configuration allows us to apply the Android Library Plugin and configure the Android code block and default dependencies

.

What if you want to create a pure Java Module that doesn’t load Android Plguin?

Simple, we can create a few more plug-ins (don’t call them buildSrc)

For example, I can create MyBasePlugin, MyAndroidPlugin, MyJavaPlugin, MyMultiplatformPlugin

.

Make your plugin configurable

.

We can use the Android {} code block configuration in the Module where the Android Library or Android Application plug-in is applied

android {
    compileSdkVersion(29)
}
Copy the code

.

The Gradle API defines this as an Extension

We can also define some custom extensions

.

For example, we want to deal with two configurations

  1. Has the Module been released?
  2. What is packageName when it is published?

.

Once configured, we can use it like this

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

test {
    publish = true
    packageName = "my-package"
}
Copy the code

.

We just need to create a class

open class TestExtension {
    var publish: Boolean = false
    var packageName: String = ""
}
Copy the code

.

Next, tell Gradle what configuration is available

// 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()
    }
}
Copy the code

.

The authors conclude that their company uses the buildSrc Plugin to accomplish the following in just seven lines of code

  • 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)

.

The above is the main information expressed by the original author, the original text please move

.

The demo code

.

English level is limited, if any mistakes please advise.