In the first six series, we have introduced Gradle’s general usage practices and some basic properties. In this part, we will take a closer look at Gradle’s Tasks and Plugins.

  1. Understand Groovy
  2. Custom Tasks
  3. In-depth Android plugin
  4. Define your own plugin

Understand Groovy

Most Android developers use Java as their development language, and Groovy isn’t much different from Java, but much easier to read. The rest of this section is a brief introduction to groovy

For more information on the use of Groovy, visit the Goovy website for more documentation

About Groovy

Goovy is a scripting language inherited from Java that runs on the JVM and is intended to be simpler and more accessible. Let’s see how Groovy works by comparing the Java and Groovy language implementations

In Java, print a string on the screen as follows

System.out.println("Hello, world!");
Copy the code

In Groovy, as follows

println 'Hello, world! 'Copy the code

You can see the following differences in Groovy’s implementation

  • There is no system.out namespace
  • No parentheses wrap method parameters
  • There are no semicolons at the end of the line

In groovy, both single and double quotation marks can be used, but there are some slight differences. The characters in double quotation marks can use placeholders. Here are some examples of using strings

def name = 'Andy'
def greeting = "hello, $name!"
def name_size = "Your name is ${name.size} characters long."
Copy the code

Grreting: “Hello,Andy”,name_size: “Your name is 4 characters long.”

Variable references to characters also support dynamic method execution

def method = 'toString'
new Date()."$method"()
Copy the code

This might sound strange in Java, but it’s common in dynamic languages like Groovy

Classes and member variables in Groovy

Creating a new class in Groovy is similar to Java, as follows: a simple Groovy class contains a member variable and a method

class MyGroovyClass { String greeting String getGreeting() { return 'Hello! '}}Copy the code

As you can see from the above classes, methods, and member variables, there are no restrictions like public or private. Unlike the default access restrictions in Java, class and Method in Groovy are public by default, and member variables are private. Using MyGroovyClass, As follows:

def instance = new MyGroovyClass() instance.setGreeting 'Hello, Groovy! ' instance.getGreeting()Copy the code

Create variables using the keyword def. Once you’ve created a new MyGroovyClass object, you can access the getGreeting() method via instance. Groovy creates get and set methods for member variables by default, such as

 println instance.getGreeting()
 println instance.greeting
Copy the code

The above two lines of code call the same instance.greeting method that calls instance.getgreeting ()

methods

Methods are defined differently in Groovy than in Java in two ways

  • Groovy does not force you to define the return type of a method
  • Groovy always adds a return statement at the end of a method

For details, please refer to the following comparison

The Java code is as follows

public int square(int num) {
       return num * num;
}
square(2);
Copy the code

Groovy code is as follows

def square(def num) {
       num * num
}
square 4
Copy the code

As you can see by comparing the two sections of code, the Groovy code does not have an int method return value constraint, nor does it have a return statement (though in practice, a return statement should be added for code readability), nor does it have parentheses wrapped around the parameters when calling a method.

There is also a more concise method declaration, which can be found in the following code

def square = { num ->
       num * num
}
square 8
Copy the code

This approach is not a standard method definition, but rather an implementation of closures, which do not exist in Java but play an important role in Groovy.

closure

A closure is a method block that can accept arguments and return results anonymously. It can be assigned to a variable or passed as a parameter. The closure definition can be illustrated in the previous code block, or it can be a little more concise

Closure square = {
       it * it
}
square 16
Copy the code

Use the def keyword when you declare it, and use Closure to make your code clearer. Groovy uses it instead of declaring arguments in closures, but only when the Closure has only one argument. Then it is null

In Gradle, most code is implemented as closures. The Android {} and Dependencies {} blocks are implemented as closures

A collection of

There are two main types of collections that use Groovy in Gradle: Lists and maps

Create a List as shown below

List list = [1, 2, 3, 4, 5]
Copy the code

Walking through a list collection is also simple

list.each(){element ->
   println element
}
Copy the code

The each method iterates over each piece of data in the list, and the method above can also be streamlined with it parameters

 list.each() {
       println it
}
Copy the code

The Map type is often used in Gradle configuration. Define a Map as follows

Map pizzaPrices = [margherita:10, pepperoni:12]
Copy the code

Getting a value from a map can be referenced using the GET method or square brackets

pizzaPrices.get('pepperoni')
pizzaPrices['pepperoni']
Copy the code

Map also supports a more concise way to value values, as shown below

 pizzaPrices.pepperoni
Copy the code

Use of Groovy in Gradle

By linking some of groovy’s basic concepts, we can better understand what configuration code in Gradle means, for example

apply plugin: 'com.android.application'
Copy the code

The meaning of this line of code, without abbreviating it, is this

 project.apply([plugin: 'com.android.application'])
Copy the code

Apply is a method of project class. The parameter is a map parameter, and only one data is used in the map. The key is “plugin” and the value is “com.android.application”.

You can also see the configuration of the dependencies

Dependencies {the compile 'com. Google. Code. Gson: gson: 2.3'}Copy the code

The code block is a closure. In the Dependencies method of project, the code not abbreviated is shown below

Project dependencies ({add (' the compile ', 'com. Google. Code. Gson: gson: 2.3', {/ / Configuration statements})})Copy the code

From the code dependencies incoming closure to DependencyHandler the add method of class, the add method to accept a configuration name (” compile “) and rely on the path to the “com. Google. Code. Gson: gson: 2.3”

For more information about Gradle configuration, see Gradle Project Introduction

Deep into the Task

Custom tasks can improve daily development efficiency, such as custom tasks to rename apK names, process version numbers, etc. Custom tasks can be run at any step in the build process, very powerful

Define the task

Tasks belong to the Project class, and each task implements the Task interface. The easiest way to define a task is to run the Task method with the name of the task as an argument

task hello{
   println 'hello world'
}
Copy the code

The hello task created by the code above, we run it and get this output

$ ./gradlew hello
Hello, world!
:hello
Copy the code

At first glance it might look like the task is running successfully, but in fact “Hello, World!” The output is before the task runs. This problem is mainly because Gradel’s Task life cycle is initialize -> Run configuration -> Run task. Task also has three syntax: initialization syntax, configuration syntax, and task instruction syntax. The above task is actually configuring the syntax, even though we are running other tasks, “Hello,world!” Will be output

The correct task creation code should look like this

 task hello << {
     println 'Hello, world!'
}
Copy the code

The only difference in the above code is the “<<” symbol, which indicates that the task executes the run task syntax instead of the configuration syntax. To compare the two, refer to the following code

task hello << {
  println 'Execution'
}
hello {
  println 'Configuration'
}
Copy the code

The output is

$ ./gradlew hello
Configuration
:hello
Execution
Copy the code

Because there are many Groovy abbreviations, there are several ways to define tasks in Gradlle

task(hello) << { println 'Hello, world! ' } task('hello') << { println 'Hello, world! ' } tasks.create(name: 'hello') << { println 'Hello, world! '}Copy the code

Task () is a Gradle Project method that takes two arguments, a string of task names and a closure. Task () is a Gradle Project method

The last is implemented not through the Task method, but through the Create method of the Tasks (instance of TaskContainer) object, which takes a map and closure as arguments

Analyze the Task

The Task interface is the basic interface for all tasks and contains some common properties and methods. DefaultTask implements this interface, and all tasks we create inherit from DefaultTask

To be exact, DefaultTask is not a real implementation of the Task interface. Gradle contains an AbstractTask class that implements the Task interface. And DefaultTask inherits AbstractTask so we create the task by inheriting DefaultTask

Each Task contains a collection of Action objects, and when a Task is executed, all these actions are executed in sequence. Adding an action to a Task can be done using the doFirst() and doLast() methods, both of which take a closure as an argument and are passed into the Action object for invocation

When creating a Task, at least one of doFirst and doLast must be implemented. In our previous writing, the left shift symbol (<<) is shorthand for the doFisrt method, as shown in the code example below

task hello {
     println 'Configuration'
     doLast {
       println 'Goodbye'
      }
     doFirst {
       println 'Hello'
      }
}
Copy the code

The output is:

$ gradlew hello
Configuration
:hello
Hello
Goodbye
Copy the code

You can see that doFirst is always executed at the beginning of a task and doLast is always executed at the end of a task, which means that order is important when using both methods, especially in logic where order is important.

If tasks need to be executed sequentially, we can use the mustRunAfter() method, which represents the sequential relationship between the execution of two methods, one of which must be executed after the other

task task1 << {
    println 'task1'
}
task task2 << {
    println 'task2'
}
task2.mustRunAfter task1
Copy the code

Running task1 and task2 at the same time will result in task2 being executed after Task1 regardless of the order used in the commands

$ ./gradlew task2 task1
:task1
task1
:task2
task2
Copy the code

The mustRunAfter() method does not add a dependency, that is, only task2 will be executed, task1 will not be executed. If you want the task to depend on another task, use dependsOn()

task task1 << {
    println 'task1'
}

task task2 << {
    println 'task2'
}

task2.dependsOn task1
Copy the code

The output is:

$ gradlew task2
:task1
task1
:task2
task2
Copy the code

With the mustRunAfter method, Task1 is always executed before Task2, but both task1 and Task2 need to run. DependsOn method is used, even if only task2 is run, since task2 dependsOn task1, task1 will execute first and then task2.

Simplify the Android packaging process with Tasks

In Android, when the function is developed and the APK is released to the Android market (Google Play and other app markets), you need to sign the APPLICATION APK package. The signature configuration is as follows:

android {
    signingConfigs {
      release {
         storeFile file("release.keystore")
            storePassword "password"
            keyAlias "ReleaseKey"
            keyPassword "password"
      }
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}
Copy the code

The configuration above is actually very insecure, some security information such as password and key are written in the code, if uploaded to Git, it is easy for others to get the information. This can be done by customizing a task to ask for a password every time it is packaged, or if this is too cumbersome, writing it in a non-version-controlled file, such as creating a private.properties file in the project root directory and ignoring it in the.gitignore file.

The contents of the private.properties file can be written like this

release.password = thepassword
Copy the code

We now define a task for getReleasePassword

task getReleasePassword << { def password = '' if (rootProject.file('private.properties').exists()) { Properties properties = new Properties(); properties.load( rootProject.file ('private.properties').newDataInputStream()) password = properties.getProperty('release.password') }else{ if (! password? .trim()) { password = new String(System.console().readPassword ("\nWhat's the secret password? ")}}}Copy the code

Properties file exists in the root directory of the current project. If so, load the file and find releas.password. To ensure that users without a Properties file can run, the user is asked for input in the console when the properties file is not found.

if (!password?.trim()) {
    password = new String(System.console().readPassword
             ("\nWhat's the secret password? "))
}
Copy the code

So the first thing I did with the code above is I said is password empty, password? .trim(), the question mark is used to call trim when password is not null. In groovy’s if statement, null characters or empty strings are false

The system.console ().readPassword() method is groovy’s way of reading password input from the console user. It returns an array of characters, so new String() is required to construct the String

After we read the password, we can copy the signature information in the Gradle configuration

KeyPassword and storePassword are assumed to be the same

android.signingConfigs.release.storePassword = password
android.signingConfigs.release.keyPassword = password
Copy the code

In Gradle packaging, the official signature is only done when a release package is released, so this task relies on the Release task and adds the following code to the build. Gradle file:

 tasks.whenTaskAdded { theTask ->
       if (theTask.name.equals("packageRelease")) {
           theTask.dependsOn "getReleasePassword"
       }
}
Copy the code

The main intention of the above code is that in the packaging process of Android, there is a task named packageRelease at the end of the typing apK. This task is to add signature information to apK. Before performing this task, you must obtain the password of keystore. < getReleasePassword > Packagerelease.dependson () Android does not have packageRelease before Gradle builds build Variant, as the specific packaging task will be dynamically generated by Build Variants. Build Variant has a packageRelease task only if you build at the start of each build

Executing the./gradlew assembleRelease command produces the following output

As you can see from the screenshot above, the package didn’t find the private.properties file, so we added some friendly hints. How to create a private.properties file and then prompt the user to enter a password on the console to complete the package

This task example gives a brief introduction to how to complete a custom task in the Android Build process. The following sections describe the Android Gradle Plugin in detail.

Go deep into the Android Gradle Plugin

Throughout Android development, most of the tasks we need to customize are associated with Android plugins (introduced via the Apply plugin: ‘com.android.application’)

Using the build process in Android Pugin will require the proper use of Build Variants, which is very easy to use, as shown below

android.applicationVariants.all { variant ->
     // Do something
}
Copy the code

“ApplicationVariants” is a collection of variants that will allow users to iterate on each variant to find applications for a particular Variant and then obtain the variants’ attributes, such as name and description, etc. If the project is an Android Libraray then applicationVariants should be changed to librayVariants

Notice that the previous code uses the all() method instead of the each() method when iterating through the contents of the collection. This is because each will only trigger before the variants will be created, whereas the All method will only trigger when a later model joins

This technique can be used to dynamically change apK names, such as adding a version number to apK names, etc. The next section explains how to dynamically change APK names

Automatically renames APK files

In the Android packaging process, the most common requirement is to rename the default APK file name by adding the version number and channel number to the apK name. For details, see the following code

android.applicationVariants.all { variant -> variant.outputs.all { output -> def builtType = variant.buildType.name def versionName = variant.versionName def versionCode = variant.versionCode def flavor = variant.flavorName outputFileName =  "app-${flavor}-${builtType}-${versionName}-${versionCode}.apk" } }Copy the code

As can be seen from the above code snippet, each build variant has a set of outputs. The Outputs of the Android App is an APK file. The output object has an attribute called outputFileName. You can modify the last APK file name by modifying outputFileName.

With the Android plug-in hook functionality, we can also create many automated tasks. Next, you’ll learn how to create a task for each Build Variant of your application.

Create new tasks dynamically

Because of how Gradle works and the ease with which tasks are built, you can build on Android and easily create your own tasks during the configuration phase. To demonstrate this power, we’ll learn how to create an Install task that not only installs APK, but also runs the application after installation. The Install task is part of the Android plug-in, but if we run the gradlew installDebug command from the command line interface to install the application, we still need to start the application manually after the installation is complete. This section describes how to create the Install task and automatically open the application home page.

First look at the applicationVariants property you used before:

android.applicationVariants.all { variant ->
    if (variant.install) {
       tasks.create(name: "run${variant.name.capitalize()}",
         dependsOn: variant.install) {
           description "Installs the ${variant.description} and runs the main launcher activity."
      }
   }
}
Copy the code

For each Build Variant, we need to check that it has a valid install task. Because the task being created to run the application will depend on the Install task. Once the installation task is verified to exist, a new task named after the Variant name is created. You need to make the new task dependent on the Variant install task, which is set to trigger the install task before running the RUN task. In the tasks.create() method, you pass in a closure that adds a task description so that when gradlew Tasks are executed the list of tasks and their description is displayed.

In addition to adding the task description, we also need to add the actual task operations. In this example, you need to start the application. You can use the Android Debugging tool (ADB) to launch the app on a connected device or emulator:

$ adb shell am start -n com.package.name/com.package.name.Activity
Copy the code

Gradle has a method called exec() that executes a command line process. For exec() to work, we need to provide an executable file that exists in the PATH environment variable, and we also need to pass all shell execution parameters using the args attribute, which takes a list of strings as an argument. As follows:

doFirst {
       exec {
           executable = 'adb'
           args = ['shell', 'am', 'start', '-n',"${variant.applicationId}/.MainActivity"]
       }
}
Copy the code

To get the full package name, you can use varaint’s applicationId property, which will also contain suffixes if different build variants of application ID do not have the same suffixes, which can cause a problem, as shown in the following example:

android {
    defaultConfig {
           applicationId 'com.gradleforandroid'
    }
    buildTypes {
        debug {
            applicationIdSuffix '.debug'
        }
}
Copy the code

Package called com. Gradleforandroid. Debug, but the path of the Activity is still the com. Gradleforandroid. The Activity. To ensure that your Activity gets the correct loading path, remove the suffix from applictionId:

doFirst {
    def classpath = variant.applicationId
    if(variant.buildType.applicationIdSuffix) {
           classpath -= "${variant.buildType.applicationIdSuffix}"
       }
    def launchClass ="${variant.applicationId}/${classpath}.MainActivity"
    exec {
        executable = 'adb'
        args = ['shell', 'am', 'start', '-n', launchClass]
   }
}
Copy the code

In the above code, you first create a variable named CLASspath based on the application applicationId. Then we find the buildType. ApplicationIdSuffix attributes provide suffix. In Groovy, you can subtract a string from another string using the minus operator. These changes ensure that running applications after apK installation does not fail with suffixes.

Create a custom Gradle plug-in

If we have a collection of Gradle tasks that we want to reuse across multiple projects, it’s easy to extract those tasks into a custom plug-in. Not only can we reuse the build logic ourselves, but we can also share it with others.

Plug-ins can be written in Groovy or in other languages that use the JVM, such as Java and Scala. In fact, most of Gradle’s Android plug-ins are written in a combination of Java and Groovy.

Creating a simple plug-in

To extract the various build logic that is stored in the build configuration file, create a plug-in in the build.gradle file. This is the easiest way to start creating custom plug-ins.

To create a plug-in, you need to create a new class that implements the plug-in interface. You will use the code you wrote earlier in this chapter to dynamically create running tasks. The plug-in class definition looks like this:

class RunPlugin implements Plugin<Project> {
     void apply(Project project) {
         project.android.applicationVariants.all { variant ->
            if (variant.install) {
               project.tasks.create(name: "run${variant.name.capitalize()}", dependsOn: variant.install) {
                         // Task definition
                  }
            }
         }
   }
}
Copy the code

The Plugin interface defines an apply() method. Gradle calls this method when plug-ins are used in build. Gradle. Project is passed as a parameter so that the plug-in can configure the project or use its methods and properties. In the previous Task example, instead of calling the Android plugin properties directly, you need to access them by accessing the Project object. Please note that we need to access the Android plug-in properties by applying the Android plug-in in the project before applying our custom plug-in. Otherwise, exceptions may be generated.

The code for task is the same as before, with only one method call modification, by calling project.exec() instead of exec(). To ensure that the build.gradle file is applied, add this line to the build.gradle file:

apply plugin: RunPlugin
Copy the code

Release the plugin

In order to publish a plug-in and share it with others, we need to move the plug-in to a separate module (or project). Standalone plug-ins have their own build files to configure dependencies and how they are published. The plug-in module generates a JAR file containing the plug-in classes and properties. We can use this JAR file to apply plug-ins to multiple modules and projects and share them with others.

As with any Gradle project, you need to create a build. Gradle file to configure the build:

apply plugin: 'groovy'

dependencies {
    implementation gradleApi()
    implementation localGroovy()
}
Copy the code

To write a standalone Gradle plug-in, you first need to apply the Groovy plug-in. The Groovy plug-in extends the Java plug-in to make it possible to build and package Groovy classes. Both Groovy and pure Java are supported, so we can mix them up if we like. You can even use Groovy to extend a Java class, or vice versa.

Two dependencies need to be included in the build configuration file: gradleApi() and localGroovy(). Add the Gradle API to access gradle-related base interfaces from custom plug-ins. LocalGroovy () is a release of the Groovy SDK that comes with the Gradle installation. Gradle provides these dependencies by default for convenience. If Gradle does not provide these dependencies by default, we need to manually download and reference them.

If we plan to distribute the plug-in publicly, we need to make sure that we specify group and version information in the build configuration file, as follows:

Group = ‘com. Gradleforandroid version =’ 1.0 ‘

To start using the code in the standalone plug-in module, also make sure to use the correct directory structure:

plugin
    |-src
        |-main
        |    |-groovy
        |        |-com.package.name
        |-resources
             |-META-INF
                 |-gradle-plugin    
Copy the code

As with other Gradle modules, you need to provide a SRC /main directory. Because this is a Groovy project, the subdirectory of Main is called Groovy instead of Java. There is also a resource subdirectory that will be used to specify plug-in properties.

Create a file called runplugin.groovy in the package directory to define the plug-in’s classes:

package com.gradleforandroid
import org.gradle.api.Project
import org.gradle.api.Plugin

class RunPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.android.applicationVariants.all { variant ->
               // Task code}}}Copy the code

To enable Gradle to find plug-ins, create a properties file and add the properties file to the SRC /main/resources/ meta-INF /gradle-plugins/ directory. The file name needs to match the ID of our plug-in. For RunPlugin, the file called com. Gradleforandroid. Run. The properties of the file content is as follows:

implementation-class=com.gradleforandroid.RunPlugin
Copy the code

The only thing the properties file contains is the package and name that implements the Plugin interface class.

When the plugin and properties file are ready, we can package the plugin using the gradlew Assemble command. A JAR file is eventually created in the output directory. If we also want to push the plugin to the Maven repository, we also need to apply the Maven plugin:

apply plugin: 'maven'
Copy the code

Next, configure the uploadArchives task as follows:

uploadArchives {
       repositories {
           mavenDeployer {
             repository(url: uri('repository_url'))
            }
        }
}
Copy the code

The uploadArchives task is a predefined task. After configuring the repository in the task, you can perform this task to publish the plug-in. The details of how to set up Maven repositories will not be covered here.

If we want to make our plug-ins public, we can consider publishing them to Gradleware’s plugin repository. The plugin repository has a large collection of Gradle plug-ins (not just specific to Android development). Details on how to distribute plug-ins can be found in the official documentation of the.

This document does not cover writing test code for custom plug-ins, but it is strongly recommended that you test your code if you plan to make your plug-in publicly available. You can find more information about writing plug-in tests in the Gradle User guide.

Use custom plug-ins

To use the plug-in, add the plug-in as a dependency on the BuildScript block. First, configure a new dependency repository. The configuration of the dependency repository depends on how the plug-in is distributed. Second, you need to configure the plug-in’s classpath in the dependency block.

If you include the locally generated JAR files we created in the previous example, you can define a flatDir repository here:

buildscript {
       repositories {
           flatDir { dirs 'build_libs' }
       }
       dependencies {
           classpath 'com.gradleforandroid:plugin'
        }
}
Copy the code

If you upload the plug-in to Maven or Ivy repositories, the configuration will be confusing. Dependency management was introduced in Chapter 3, “Managing dependencies,” so it will not be repeated here.

After setting the dependency, you need to apply the plug-in:

apply plugin: com.gradleforandroid.RunPlugin
Copy the code

With the Apply () method, Gradle creates an instance of the plug-in class and executes the plug-in’s own Apply () method, and we can use the custom plug-in normally.

conclusion

In this chapter, we learned how Groovy differs from Java and how to use Groovy in Gradle. We also saw how to create custom tasks and hook tasks into Android plug-ins.

In the final part of this chapter, you also looked at how to create plug-ins and ensure that you can reuse them in multiple projects by creating a single plug-in. In fact, there are more in-depth knowledge than can be covered in this document. For more information, please refer to the Gradle User guide.