As an Android developer, if your build.gradle is built from an IDE or copied and pasted from another project, you should read this article to get a grasp of the basics of Gradle that you don’t know.

All the pictures in this article come from the network and are deleted

Gradle is a jVM-based build tool. Currently, all projects built in Android Studio are based on Gradle. The features of Gradle and other build tools (Ant, Maven) include:

  • Powerful DSL and rich Gradle API
  • Gradle is groovy
  • Strong dependency management
  • Can expand sex
  • Integration with other build tools

Three build scripts

Gradle scripts are configuration scripts. Each script type is actually a delegate of a class object in a specific Gradle API. The script executes the configuration of the delegate object. In a complete Gradle build system, there are three types of build scripts, each corresponding to three delegate objects

The script type Delegate object
Init script Gradle
Settings script Settings
Build script Project

init.gradle

The Init script above is actually a Gradle object delegate, so any property references and methods called in this Init script will be delegated to this Gradle instance.

The execution of the Init script occurs before the start of the build and is the earliest step in the entire build.

Configure Init Scrip dependencies

Each script execution can configure the dependencies needed for the current script’s own execution. The Init scrip configuration is as follows:

// The initScript configuration block contains the configuration required for the execution of the script itself
// We can configure dependency paths and so on
initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'org.apache.commons'.name: 'commons-math'.version: '2.0'}}Copy the code

Using the Init scrip

There are several ways to use a defined Init scrip

  • When running gradle, specify the path of the script with the -i or –init-script command options

    This approach can be tailored to a specific build.

  • Place an init.gradle file in the *USER_HOME*/. Gradle/directory

  • Place a file with a name ending in. Gradle in the gradle distribution *GRADLE_HOME*/init.d/

    Both of these approaches are global and have an effect on the build in the machine

settings.gradle

This corresponds to the Settings script script type, which is the delegate of the Settings object. Any property references and methods invoked in the script are delegated to the Settings instance.

The execution of Settings script occurs during the initialization phase of Gradle’s build life cycle. The Settings script file declares the configuration required for the build and is used to instantiate the project hierarchy. When executing the Settings script and initializing the Settings object instance, a rootProject object is automatically built and participated in the entire build. (The default name of rootProject is the name of its folder and its path is the path containing the setting script file).

Here is a class diagram of the Settings object:

For every project object added to the build process through the include method, an instance of the ProjectDescriptor is created in the Settings script.

Therefore, in the Settings script file, we have access to the objects we use:

  • Settingsobject
  • Gradleobject
  • ProjectDescriptorobject

Get Settings file

In Gradle, as long as the root project/any subproject directory contains component files, the build can be run at the appropriate location. To determine whether a build is a multi-project build, look for the Settings script file, which indicates whether subprojects are included in a multi-project build.

To find the Settings file, do the following:

  1. Search for the setting file in the master directory at the same level as the current directory
  2. If the Settings file is not found in 1, look for the Settings file in the parent directory starting from the current directory.

When the Settings file is found and the current directory is included in the file definition, the current directory is considered part of a multi-project build.

build.gradle

This corresponds to the Build script type mentioned earlier, which is the delegate of the Project object in Gradle. Any property references and methods invoked in the script are delegated to the Project instance.

Configuring script dependencies

In build.gradle there is a configuration block buildScipt{} to configure the path configuration required for the current script execution (similar to initScript).

buildscript {
	// The configuration block here is repositories separate from the one in the Project instance
	// The repository repository repository repository that the script itself depends on is delegated to ScriptHandler
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
    // As in the previous configuration block, separate it from the Dependencies configuration block in Project
    dependencies {
        classpath 'com. Android. Tools. Build: gradle: 3.1.2'}}Copy the code

To add a key point, no matter where the buildScript{} configuration block is placed in the build.gradle file, it is always executed first in the entire script file

Three building blocks

Every Gradle build contains three basic building blocks:

  • project
  • task
  • property

Each build contains at least one project, which in turn contains one or more tasks. Projects and tasks expose properties that can be used to control builds.

Project

Much of what we know about project comes from the build.gradle file in the project directory (because it is really the delegate for the project object). The class diagram for the Project object looks like this:

Project configuration

In the build.gradle script file, you can not only configure individual projects, but also define common logic for project blocks, as defined below.

Common examples are:

// Add the repository source configuration for all projects
allprojects {
    repositories {
        jcenter()
        google()
    }
}
// Add the mavenPublish configuration block for all subprojects
subprojects {
    mavenPublish {
        groupId = maven.config.groupId
        releaseRepo = maven.config.releaseRepo
        snapshotRepo = maven.config.snapshotRepo
    }
}
Copy the code

Task

Tasks are one of the basic configuration blocks that Gradle builds, and the execution of gradle builds is the execution of tasks. The following is a class diagram for Task.

Task configuration and actions

When we set a task, we will include two parts of the content: configuration and action. For example:

task test{
    println("Here's the configuration.")
    
    doFirst{
        // do something here
    }
    doLast(){
        // do something here}}Copy the code

Task action declarations currently contain two main methods:

  • doFirst
  • doLast

These actions are invoked during the execution phase of gradle’s build life cycle. It is worth noting that a task can declare multiple doFirst and doLast actions. You can also add actions to tasks defined in existing plug-ins. Such as:

// Add a doLast action to the test task
test.doLast{
    // do something here
}
Copy the code

In the definition of task, in addition to action blocks are configuration blocks, where we can declare variables, access properties, call methods, and so on. The content of these configuration blocks occurs during the configuration phase of Gradle’s build life cycle. So the configuration in the Task is executed every time. The action block is executed only when the task is actually called.

The task dependency

The order in which tasks are executed in Gradle is indeterminate. With dependencies between tasks, Gradle ensures that the dependent task will be executed first by the current task. A dependsOn() method for a task allows us to declare one or more task dependencies for our task.

task first{
    doLast{
        println("first")
    }
}

task second{
    doLast{
        println("second")
    }
}

task third{
    doLast{
        println("third")
    }
}

task test(dependsOn:[second,first]){
    doLast{
        println("first")
    }
}

third.dependsOn(test)

Copy the code

The type of task

By default, our common task is org. Gradle. API. DefaultTask type. However, gradle has a rich set of task types that we can use directly. To change the type of task, we can refer to the following example

task createDistribution(type:Zip){
    
}
Copy the code

For more information about the types of tasks, see gradle’s official documentation

Property

Properties are used throughout gradle builds to help control the existence of build logic. Gradle can declare properties in two main ways:

  • Use the ext namespace to define extended attributes
  • Use the Gradle properties filegradle.propertiesCustom properties

Ext namespace

Many model classes in Gradle provide special attribute support, such as Project. Inside Gradle, these properties are stored as key-value pairs. Using the Ext namespace, we can easily add attributes. The following methods are supported:

// Add a property named groupId to project
project.ext.groupId="tech.easily"
// Add attributes using ext blocks
ext{
    artifactId='EasyDependency'
    config=[
            key:'value']}Copy the code

Note that we only need to use the Ext namespace when declaring attributes, and that the ext namespace can be omitted when using attributes.

Properties file

As we often see in Android projects, we can create a new gradle.properties file at the root of the project and define simple key-value pair properties in the file. These properties can be accessed by gradle scripts in your project. As follows:

# gradle.properties
Note that the file's comments begin with #
groupId=tech.easily
artifactId=EasyDependency
Copy the code

The java.util.properties class can be used to dynamically create Properties files in code (for example, when customizing plug-ins) and read Properties from the files. Such as:

void createPropertyFile() {
    def localPropFile = new File(it.projectDir.absolutePath + "/local.properties")
    def defaultProps = new Properties()
    if(! localPropFile.exists()) { localPropFile.createNewFile() defaultProps.setProperty("debuggable".'true')
        defaultProps.setProperty("groupId", GROUP)
        defaultProps.setProperty("artifactId", project.name)
        defaultProps.setProperty("versionName", VERSION_NAME)
        defaultProps.store(new FileWriter(localPropFile), "properties auto generated for resolve dependencies")}else {
        localPropFile.withInputStream { stream ->
            defaultProps.load(stream)
        }
    }
}
Copy the code

The important thing about properties is that properties can be inherited. Attributes defined in a project are automatically inherited by its children, regardless of which way we add attributes.

Build life cycle

As mentioned earlier, gradle has multiple script types and they are all executed in different life cycles.

The three stages

In gradle builds, the build lifecycle consists of three phases:

  • Initialization

    As mentioned earlier, at this stage, the Settings script is executed so that Gradle confirms which projects will participate in the build. Then create a Project object for each Project.

  • Configuration

    After configuring the Project object created during Initialization, all configuration scripts are executed. (Configuration blocks including tasks defined in Project are also executed)

  • Execution (Configuration)

    Gradle determines which tasks created and configured in the Configuration phase will be executed and which tasks will be executed depending on Gradle command parameters and the current directory

Listening life cycle

During the gradle build process, Gradle provides a rich set of hooks to help us customize the build logic to the needs of the project, as shown in the following figure:

There are two main ways to listen for these life cycles:

  • Adding listeners
  • Use the configuration block of the hook

Gradle and Project define the available hooks. Common hooks include:

Gradle

  • beforeProject()/afterProject()

    Equivalent to beforeEvaluate and afterEvaluate in Project

  • settingsEvaluated()

    The Settings script is executed and the Settings object is configured

  • projectsLoaded()

    All projects that participate in the build are created from Settings

  • projectsEvaluated()

    All projects involved in the build have been evaluated

TaskExecutionGraph

  • WhenReady ()

    Task graph generation. All tasks that need to be executed have established dependencies between tasks

Project

  • BeforeEvaluate ()
  • AfterEvaluate ()

Dependency management

One of the main features of Gradle mentioned earlier is powerful dependency management. Gradle has a rich set of dependency types and supports multiple dependency repositories. Also, every dependency in Gradle is managed in groups based on a specific scope.

Add dependencies to your project in Gradle like this:

// build.gradle

// Add the dependency repository source
repositories {
    google()
    mavenCentral()
}
// Add dependencies
// Dependency types include file dependency, project dependency, and module dependency
dependencies {
    // local dependencies.
    implementation fileTree(dir: 'libs'.include: ['*.jar'])... }Copy the code

Four dependency types

There are four types of dependencies in Gradle:

  • Module is dependent on

    This is a common dependency type in Gradle, and it usually points to a component in the repository, as follows:

    dependencies {
        runtime group: 'org.springframework'.name: 'spring-core'.version: '2.5'
        runtime 'org. Springframework: spring - the core: 2.5'.'org. Springframework: spring - aop: 2.5'
        runtime(
            [group: 'org.springframework'.name: 'spring-core'.version: '2.5'],
            [group: 'org.springframework'.name: 'spring-aop'.version: '2.5']
        )
        runtime('org. Hibernate: hibernate: 3.0.5') {
            transitive = true
        }
        runtime group: 'org.hibernate'.name: 'hibernate'.version: '3.0.5'.transitive: true
        runtime(group: 'org.hibernate'.name: 'hibernate'.version: '3.0.5') {
            transitive = true}}Copy the code

    Modules depend on the ExternalModuleDependency object in the API corresponding to Gradle

  • File is dependent on

    dependencies {
        runtime files('libs/a.jar'.'libs/b.jar')
        runtime fileTree(dir: 'libs'.include: '*.jar')}Copy the code
  • Project depend on

    dependencies {
        compile project(':shared')}Copy the code

    The project depends on the ProjectDependency object in the API corresponding to Gradle

  • Specific Gradle distribution dependencies

    dependencies {
        compile gradleApi()
        testCompile gradleTestKit()
        compile localGroovy()
    }
    Copy the code

Managing dependency Configuration

Each dependency of a gradle project is applied to a specific scope and is represented by a Configuration object in Gradle. Each Configuration object has a unique name. Gradle dependency configuration management looks like this:

The custom Configuration

In Gradle, it is very easy to customize a Configuration object. When you define your own Configuration object, you can also inherit the existing Configuration object, as shown below:

configurations {
    jasper
    // Define the inheritance relationship
    smokeTest.extendsFrom testImplementation
}

repositories {
    mavenCentral()
}

dependencies {
    jasper 'org, apache tomcat. Embed: tomcat embed - jasper: 9.0.2'
}

Copy the code

Manage transitive dependencies

In actual project dependency management there is a dependency relationship like this:

  • Module B depends on module C
  • Module A depends on module B
  • Module C becomes a transitive dependency of module A

Gradle provides powerful administrative capabilities when dealing with transitive dependencies like these

Using dependency constraints

Dependency constraints can help us control the version numbers (version ranges) of transitive dependencies and our own dependencies, such as:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient'
    constraints {
        // HttpClient is a dependency of the project itself
        // This constraint means that the specified version number is enforced either for the project's own dependencies or for passing dependencies
        implementation('org, apache httpcomponents: httpclient: 4.5.3') {
            because 'previous versions have a bug impacting this application'
        }
        Commons-codec is not declared as a dependency of the project itself
        // So this logic is only triggered if Commons -codec is transitive
        implementation('Commons - codec: the Commons - the codec: 1.11') {
            because 'version 1.9 pulled from httpclient has bugs affecting this application'}}}Copy the code

Exclude specific transitive dependencies

Sometimes we rely on projects/modules that introduce multiple transitive dependencies. However, we do not need some transitive dependencies. In this case, we can exclude some transitive dependencies by using exclude as follows:

dependencies {
    implementation('the log4j: log4j: 1.2.15') {
        exclude group: 'javax.jms'.module: 'jms'
        exclude group: 'com.sun.jdmk'.module: 'jmxtools'
        exclude group: 'com.sun.jmx'.module: 'jmxri'}}Copy the code

Enforces the use of the specified dependency version

Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency diagram. Sometimes, however, some projects need to use an older version number as a dependency. At this point we can force a version to be specified. Such as:

dependencies {
    implementation 'org, apache httpcomponents: httpclient: 4.5.4'
    // Assume that the latest version of Commons-Codec is 1.10
    implementation('Commons - codec: the Commons - the codec: 1.9') {
        force = true}}Copy the code

Note that if a dependency project uses a newer version of the API and we force an older version of the transition-dependency, it will cause runtime errors

Transitive dependencies are disabled

dependencies {
    implementation(: 'com. Google. Guava guava: 23.0') {
        transitive = false}}Copy the code

Dependency resolution

Use dependency resolution rules

Dependency resolution rules provide a very powerful way to control the dependency resolution process and can be used to implement various advanced patterns in dependency management. Such as:

  • The version of the unified component group

    Many times we rely on a company’s library to contain multiple modules, which are generally uniformly built, packaged, and distributed with the same version number. At this point we can achieve version number unification by controlling the dependency resolution process.

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'org.gradle') {
                details.useVersion '1.4'
                details.because 'API breakage in higher versions'}}}Copy the code
  • Handles custom versions of Scheme

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.version == 'default') {
                def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
                details.useVersion version.version
                details.because version.because
            }
        }
    }
    
    def findDefaultVersionInCatalog(String group, String name) {
        //some custom logic that resolves the default version into a specific version
        [version: "1.0".because: 'tested by QA']}Copy the code

For more examples of dependency resolution rules, see ResolutionStrategy in gradle’s API

Use substitution rules for dependencies

The dependency substitution rules are somewhat similar to the dependency resolution rules above. In fact, many of the functions of dependency resolution rules can be achieved by dependency replacement rules. Dependency substitution rules allow Project Dependency and Module Dependency to be transparently replaced by specified substitution rules.

// Replace module dependencies with project dependencies
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module("org.utils:api") with project(":api") because "we work with the unreleased development version"
        substitute module("Org. Utils: util: 2.5") with project(":util")}}// Replace project dependencies with module dependencies
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute project(":api") with module("Org. Utils: API: 1.3") because "we use a stable version of utils"}}Copy the code

In addition to the above two, there are three other dependency rules. Customizing Dependency Resolution Behavior Customizing Dependency Resolution Behavior Customizing Dependency Resolution Behavior Customizing Dependency Resolution Behavior

  • Use meta-data rules for components
  • Use component selection rules
  • Use the module replacement rule

Plug-in development

Plug-in development is a powerful tool in Gradle’s flexible build system. Using the PluginAPI in Gradle, you can customize your plugins and use common build logic as plugins. Such as are used in the Android project: com. Android. Application, kotlin – Android, Java, and so on.

There are plenty of articles about plug-in development on the web, and I won’t go into them here. Gradle dependency management is a plugin for Gradle dependency management. It is a plugin for Gradle dependency management.

  • EasyDependency

    Gradle is a gradle plugin that helps improve the efficiency of componential development.

    1. The components of the publishing module are remote Maven repositories
    2. Dynamic replacement dependency configuration: Use source dependencies or maven repository component (AAR/JAR) dependencies on modules

Write in the last

This paper is basically organized and summarized according to my own ideas after reading gradle’s official documents and related materials. Welcome to discuss gradle usage and questions.