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.
- Understand Groovy
- Custom Tasks
- In-depth Android plugin
- 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.