Before we start this section, let’s review a few key words:

  • Project → Build Gradle → Consist of one or more projects;
  • Task → Project in Gradle → Consists of one or more tasks;
  • Actions (perform actions) → Tasks in Gradle → Consist of one or more actions (functions/methods) in order;

Then go back to the Gradle build lifecycle mentioned in the previous section:

In Gradle, they are configuration scripts. When scripts are executed, they actually configure an object of a particular type:

Init Script → Gradle object, Settings Script → Settings object, Build Script → Project object;

This object is also called the script’s proxy object, and every property and method on the proxy object can be used in the script.

Each Gradle Script implements the Script interface, which consists of zero or more Script statements and Script blocks. Script statements can contain function calls, attribute assignments, and local variable definitions, while a script block is a method call passed in a configuration closure that is executed to configure the proxy object.

This section covers the build lifecycle and dependency rules & dependency conflict handling. This section is a bit boring, but I will keep it brief and easy to understand. I hope it will help you solve Android multi-module building problems.

0x1. Initialization

1. Init Script

The execution sequence of files and scripts involved is as follows:

$GRADLE_USER_HOME/init. Gradle. (KTS) – > $GRADLE_USER_HOME/init. D / [*. Gradle. (KTS)]

This step generates Gradle objects that provide three types of apis: fetching global properties, project configuration, and lifecycle hooks. Some of the apis are as follows:

// To obtain an instance of Gradle, call. Gradle or project.getgradle () in the *. Gradle file.

/* ========= ① Obtain the global attribute ========= */GradleHomeDir → Gradle directory to execute this build; GradleUserHomeDir → Gradle UserHome; GradleVersion → Current Gradle version; IncludedBuilds → Get embedded builds; Parent → get parent build; PluginManager → Get plug-in manager instance; Plugins → Get plug-in container; RootProject → get the rootProject of the current build; TaskGraph → Get the task graph that is currently being built. This object does not have content until taskgraph. whenReady {}/ * = = = = = = = = = (2) the Project configuration, the closure method can be carried in the Project as soon as available = = = = = = = = = * /

rootProject(action) // Add a closure for Root Project
allprojects(action) // Add closures for all projects
Copy the code

Example: $GRADLE_USER_HOME/init.gradle(.kts) = $GRADLE_USER_HOME/init.d/ = XXX. Gradle (.ktx) = XXX. As follows:

// The project depends on the repository
allprojects {
    repositories {
        maven { url "https://maven.aliyun.com/repository/google" }
        maven { url "https://maven.aliyun.com/repository/jcenter"}}}// Gradle scripts depend on repositories
gradle.projectsLoaded {
    rootProject.buildscript {
        repositories {
            maven { url "https://maven.aliyun.com/repository/google" }
            maven { url "https://maven.aliyun.com/repository/jcenter"}}}}Copy the code

After Gradle project is configured, it will download the dependencies from the Maven repository first, and then download them from the repository configured in the project. In the Gradle compilation speed, you can replace the Maven address with your own private server


2. Settings Script

Involved files: settings.gradle(.kts) in the root directory of the project. In this file: declare the modules to be built with parameters and the plug-ins needed to manage the build process.

Gradle starts looking for this file in the current directory and recursively searches for it in the parent directory. Therefore, it is recommended to have a settings.gradle(.kts) file for both single and multiple projects.

① Declare the parameters to build the module

The most important method in Settings is include(String… ProjectPaths) method, used to add participating projects, passing in a variable parameter that is the path of each Project (the path of the current Project relative to the root Project), as shown in the following example:

// [:] represents the project separator, similar to the [/] in the path separator. The beginning of a: indicates relative to the root directory
include ':module1'
include ':libs:library1'
// You can also write on one line
include ':module1'.':libs:library1'

// Note: Use relative path description when subitems are not in the root directory
project(":module3").projectDir = File(rootDir, ".. /.. /library2")

// By default, Gradle uses the name of the root project directory as the project name
// When used with CI, a random file name is often detected and the project name can be forcibly specified
rootProject.name = 'JustTest'
Copy the code

Each included project generates a ProjectDescriptor object that describes the module. Module name can eventually added to the Map type DefaultProjectRegistry. The projects, so no special treatment include order.

In addition, settings.gradle(.kts) loads the Build Script in the current directory even if it doesn’t write anything.

② Manage the plug-ins needed for the build process

Through the Settings. PluginManagement related interface implementations, such as specifying the warehouse address (default from Gradle official create warehouse lookup), open the Settings. Gradle:

pluginManagement {
    // Corresponds to the PluginManagementSpec class
    repositories { // Manage Plugin Repository
        google { url "https://maven.aliyun.com/repository/gradle-plugin" }
    }
}

rootProject.name = 'temp'
include ':module1'.':module2'
Copy the code

Using the resolutionStrategy interface, you can make plug-in decisions, such as printing information for a Kotlin project plug-in:

resolutionStrategy { 
    eachPlugin {   // Receive a closure of type PluginResolveDetails, which can be accessed from the RequestSD
        println ${requested.id} → ${requested.module} → ${requested.version}"}}Copy the code

The following output is displayed:

You can then replace the plug-in by ID or specify the plug-in version, as shown in the following example:

ResolutionStrategy {eachPlugin {// Accept a closure of type PluginResolveDetails, ${requested.id} → ${requested.module} → ${requested.version}" // replace the module if (requested.id.namespace == "org.jetbrains.kotlin") { UseModule (" org. Jetbrains. Kotlin. The JVM: org. Jetbrains. Kotlin. The JVM. Gradle. The plugin: ${requested. Version} ")} / / plug-in version if unity (requested.id.id == "org.jetBrains. Kotlin.jvm ") {useVersion("1.3.71")}}}Copy the code

In addition, there are two life cycle events involved in this phase: settingsEvaluated(), which gets the configured Setting object, and projectLoaded(), which gets the Project object containing the basic information of the Project.


3. Build Script

Related files: build.gradle(.kts) in the module directory, used to configure the building information of the current module, divided into:

  • The Root Build Script for the Root module (usually a uniform configuration of submodules without much content);
  • Submodule Build Script;

Build Script: Init Script → Settings Script → Root Build Script(single module does not have this step) → Build Script(default alphabetic order, can be set by dependency intervention)

Build Script does two things: plug-in introduction and property configuration, which further configures the Project object to generate a directed acyclic graph of the Task.

① Plug-in introduction

Gradle itself does not provide compilation packaging. It is a framework that defines processes and rules. All compilation is done by plug-ins, such as Java with Java plug-in and Kotlin with Kotlin plug-in.

So what exactly is a plug-in? → ANSWER: Define tasks and execute templates for those tasks.

There are two types of plug-ins:

  • Script plug-in: a piece of script code that exists in another script file;
  • Binary plug-ins (compiled into bytecode) : Implement the Plugin interface and programmatically manipulate the build process (in the form of a project or Jar package);

Gradle built-in some core plug-in, and provides a simple name, such as “Java”, not in the plug-in needs to adopt full name, such as: “org. Jetbrains. Kotlin: kotlin – Gradle – plugin”, this is also called the plug-in id, only not repeat! The differences of introduction methods are as follows:

// Plugins can also be used, but some plugins cannot be specified. Some plugins must be specified. // The following is an infix expression from Kotlin, Plugins {id(" org.jetBrains. Kotlin.jvm ") version "1.3.71" id(" org.jetBrains. Kotlin.jvm ") version "1.3.71" apply false Java 'build-scan'} Buildscript {repositories {jCenter ()} dependencies {classpath "Org. Jetbrains. Kotlin: kotlin - gradle - plugin: 1.3.72"}}Copy the code

② Attribute Configuration

Once a plug-in is applied, you can intervene in the module building process by configuring it using the DSL provided by the plug-in. Take an Android build as an example:

// Add an Android {} configuration closure to the Project object
apply plugin: 'com.android.application'

android {
    compileSdkVersion 29    // Compile this module using API 29

    // Some configuration at compile time
    defaultConfig {
        applicationId "com.example.test"
        minSdkVersion 26
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    // Signature configuration
    signingConfigs {
        release {
            storeFile file('test.jks')
            storePassword '123456'
            keyAlias 'test'
            keyPassword '123456'}}// Build type configuration
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
    
    // Compile option configuration
    compileOptions{
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
Copy the code

In addition to the attribute DSLS introduced by the plug-in, the Project object also provides a number of DSLS for configuring builds, such as Dependencies to configure and compile dependencies.

In addition, the root Build Script can use an Ext attribute to share data between projects and unify the module-dependent versions.

// Build. Gradle configuration in the root directory
ext {
    applicationId = "xxx.xxx.xxx"
    buildToolsVersion = "28.0.3"
    compileSdkVersion = 28
    minSdkVersion = 22. }// The build.gradle submodule is used
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    ...
}
Copy the code

0x2. Dependency rules

Gradle declares the scope of each dependency, which can be grouped. For example, some dependencies are used at compile time and others are used at run time. Gradle uses a Configuration to represent this scope (grouping).

Many Gradle plugins preset some Configuration to add to your project, such as Java plugins:

It doesn’t matter. Take a look at these built-in Android configurations (2.x is deprecated) :

// compile for 2.xImplementation → Current module dependency, but do not expose this dependency to other modules, compile time can only be accessed in this module; API → current module dependency, and expose this dependency to other modules, same as compile; For example, module A and module B, if A relies on Gson library, B relies on A, use implementation, B can not use Gson, use API, B can use Gson.// Provided/apk = 2CompileOnly → valid at compile time and does not participate in packaging; RuntimeOnly → valid at runtime and will not participate in compilation; AnnotationProcessor → annotationProcessor depends on testCompile → corresponding2.The testImplementation of X, valid only when the unit test code is compiled and the test APK is finally packaged; DebugCompile to corresponding2.X debugImplementation, only for debug mode compilation and final DEBUG APK package; ReleaseCompile → releaseImplementation, only when a release is compiled and the final release APK is packaged; P.S. four ways of dependence// ① Local library dependency
implementation project(":mylibrary")  

// local binary dependency
implementation files('libs/xxx.jar'.'libs/yyy.jar')    // Rely on specific libraries
implementation fileTree(dir: 'libs'.include: ['*.jar']) // Depend on the library in the directory

// ③ Remote binary dependency
implementation('the IO. Reactivex: rxandroid: 1.2.1')

// ④ AAR package dependencies
implementation(name: 'me.leolin:ShortcutBadger'.ext: 'aar')    / / local
implementation 'me. Leolin: ShortcutBadger: 1.1.17 @ aar'    / / remote
Copy the code

Of course, you can also customize a Configuration, as shown in the following example:

allprojects {
    // Configure the maven repository address
    repositories {
        maven { url "https://maven.aliyun.com/repository/jcenter"}}}// Define a Configuration named myDependency
configurations {
    myDependency
}

// Add a dependency for the custom Configuration
dependencies  {
    myDependency('the IO. Reactivex: rxjava: 1.1.9')
    myDependency('the IO. Reactivex: rxandroid: 1.2.1')}// Prints the address of the customized Configuration file
task showMyDependency {
    println configurations.myDependency.asPath
}
Copy the code

Type gradle showMyDependency and the output will look like this:

You can also call the extendsFrom method to inherit all dependencies of another Configuration, such as compile, inherited by implementation.

Finally, there are two flags to mention, both of which default to true:

  • CanBeResolved: compile-time dependencies
  • CanBeConsumed: Runtime dependencies

0x3 dependency creation process

Tips: with the source code to understand the principle, not interested in can directly skip, does not affect subsequent learning ~

1. Dependency identification

Build. Gradle < span style = “box-sizing: border-box! Important;

The passed closure is executed by the Project DependencyHandler to locate this interface:

You can see that each method returns a Dependency instance. Click on this interface:

DependencyHandler () {add(); DependencyHandler () {add();

implementation(io.reactivex:rxandroid:1.21.) {
    transitive = false
}

Dependency add(String configurationName, Object dependencyNotation, Closure configureClosure);

// Parameter one-to-one correspondence:Implementation, IO. Reactivex: rxandroid:1.21., followed by curly braces (dependency configuration closures)Copy the code

Gradle implements DependencyHandler () class implements DependencyHandler ()

This class implements a DependencyHandler interface and implements a **MethodMixIn** interface:

Before explaining this interface, let’s take a look at two Groovy language features: invokeMethod and methodMissing.

package groovy.reflect

class InvokeTest1 {
    def hello() {
        'Execute Hello method'
    }
    
    def invokeMethod(String name, Object args) {
        return $name(${args.join(',')})
    }
    
    static main(args) {
        def it = new InvokeTest1()
        println it.hello()
        println it.foo("test".28)}}// Run output:
// Execute the Hello method
// Foo (test, 28)
Copy the code

For a method call on an object, the class assigns the method to the method if it exists, or calls the invokeMethod method if it cannot be dispatched. MethodMissing does the same thing:

package groovy.reflect

class InvokeTest1 {
    def hello() {
        'Execute Hello method'
    }
    
    def methodMissing(String name, Object args) {
        return $name(${args.join(',')})
    }
    
    static main(args) {
        def it = new InvokeTest1()
        println it.hello()
        println it.foo("test".28)}}Copy the code

The output is the same, whereas in Groovy invokeMethod is used to distribute all of an object’s methods (implemented and unimplemented), using the GroovyInterceptable interface. MethodMissing, on the other hand, can distribute only methods that are not implemented by a class, whether or not it implements the GroovyInterceptable interface.

To summarize: invokeMethod manages all methods, methodMissing only manages all unimplemented methods of a class!

So back to the MethodMixIn interface, which is essentially a Gradle wrapper around methodMissing. To implement this, all a class needs to do is implement this interface, The interface defines an abstract method getAdditionalMethods() that returns a MethodAccess object:

GetAdditionalMethods defines two methods: to determine if a Method exists, and to dynamically execute methods.

Here is where to assign the dynamicMethods attribute:

With the DynamicAddDependencyMethods:

Dependencyadder.add () is a dependencyAdder interface.

This is actually the doAdd() method of the DefaultDependencyHandler called:

Determine if the Configuration notation object is a Configuration object. If so, make the current Configuration object inherit the dependencyNotation object. Dependencies that are to be added to the dependencyNotation are added to the configuration.

2. Create a dependency

Further down, DefaultDependencyHandler calls the create() method to create an instance of a Dependency, then:

DefaultDependencyFactory → createDependency()

DependencyNotationParser () creates Dependency instance (up and down) :

The constructor sets this parameter, following where it is passed:

DependencyNotationParser → Parser ()

You can see several types of NotationConverter:

/ / 1) DependencyStringNotationConverter, DependencyMapNotationConverter for the below:
implementation(io.reactivex:rxandroid:1.21.) {
    transitive = false
}

/ / (2) DependencyFilesNotationConverter for the below:
implementation fileTree(dir:'libs'.include:['*.jar'])

/ / (3) DependencyProjectNotationConverter for the below:
implementation project(":applemodule")

/ / (4) DependencyClasspathNotationConverter against claspath depend on the situation
Copy the code

SelfResolvingDependency: SelfResolvingDependency: SelfResolvingDependency: SelfResolvingDependency: ProjectDependency: SelfResolvingDependency

SelfResolvingDependency is a self-resolving dependency independent of Repository. The latter relies on another project:

Let’s follow it

3. ProjectDependency

With the: DependencyProjectNotationConverter

With: DefaultProjectDependencyFactory – > create ()

Instantiator.newinstance is used to instantiate an object with DSL features, where a ProjectDependency instance is returned, and another create() method passes an extra Configuration name.

This is the introduction to dependency creation. Continue on artifacts, and the following sections will summarize:

  • (1) DependencyHandler does not implement implementation, API, etc., and uses MethodMissing mechanism to invoke these methods indirectly.
  • (2) Different dependency declarations are transformed into SelfResolvingDependency and ProjectDependency by different converters.

0x4 Dependency Conflict Resolved

When it comes to practical problem solving, dependency conflicts are inevitable when modularizing or relying on other people’s open source libraries

1) XXX Not Fount

  • Compile time: usually does not rely on the correct library results;
  • Runtime: Use of compileOnly causes some libraries to be compile-only dependent.

② Program type already present com.xxx.xxx

To view the dependencies tree, click on Tasks → Android → Android Dependencies.

You can also print the dependency tree to a specific file for easy retrieval by executing the following command:

./gradlew :app:dependencies > dependencies.txt

# Can also be viewed by circumstances, such as:
gradlew :module_base:dependencies --configuration api

Check the dependencies of the specified library/gradlew :app:dependencyInsight - dependency specifies the library - configuration compile# Use build Scan to analyze dependencies and generate HTML readability and better interface
./gradlew build --scan
Copy the code

The next step is to locate the offending class according to the result of compiling the error, and then to find the package corresponding to the conflict in the dependency tree, and then to decide on various methods to handle the conflict.

③ Exclude & disable dependency passing

If you open the dependency tree above, you will see that some dependencies are marked with an asterisk (*) to indicate that the dependency is being ignored.

This involves passing dependencies. When Gradle parses a library, it automatically downloads its dependencies and its dependencies (recursively), and then processes versions of these dependencies.

A simple solution is to refer to a dependency library and exclude the one that causes the conflict.

implementation("IO. Reactivex. Rxjava2: rxandroid: 2.1.1") {
    exclude(group = "io.reactivex.rxjava2", module = "rxjava")
    exclude(group = "io.reactivex.rxjava2")}// Global configuration is excluded
configurations {
	compile.exclude module: 'xxx'
	all*.exclude group:'xxx.xxx'.module: 'XXX' 
}
Copy the code

Another option is to disable dependency passing, as shown in the following example:

implementation("IO. Reactivex. Rxjava2: rxandroid: 2.1.1") {
    transitive = false
}

// Globally disabled
configurations.all {
    transitive = false
}
Copy the code

④ Use the current version forcibly

By adding isForce == true to enforce the use of a specific version, multiple versions of the same module are force, whichever is the first:

dependencies {
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.6." ") {
        isForce = true
    }
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.10") {
        isForce = true}}Copy the code

The final version of the above is 2.2.6, which only applies to the current module, and different modules are independent of each other. Note that: a dependent version of the module force is lower than the main module version, so try not to use a dependent version of the module force in the lib.

Of course, it is also possible to force the main project (high or low version), or to replace the dependent version with the following code:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if(! requested.name.startsWith("multidex")) {
                details.useVersion '27.1.1'//27.1.1 is the current version}}}}Copy the code

So far, we have learned that these are the solutions we have used. If there are any omissions, welcome to add them in the comment section. Thank you ~

0x5 dependency rules

1. Share dependent versions among multiple projects

Gradle now supports separate dependency and version declarations. Instead of using ext to specify dependency library versions in multiple projects, we can use the following method:

// Subproject Version, which is dedicated to managing releases and sharing releases using Constraint
dependencies {
    constraints {
        implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.0." ")}}// Other subprojects use platform to import all dependency constraints for submodules
dependencies {
    implementation(platform(project(":version")))

    // No version needed
    implementation("io.reactivex.rxjava2:rxjava")}// Platform → require dynamics, to introduce strictly dynamics, use enforcedPlatform
Copy the code

2. Rely on constraint strength

  • Required → I rely on a version, but if another dependency provides a higher version, the higher version is preferred, by default.
  • Strictly → strictly restrict a version, often used to degrade a dependent version, as shown in the following example:
Dependencies {implementation (" IO. Reactivex. Rxjava2: rxandroid: 2.1.1 ") Implementation (" IO. Reactivex. Rxjava2: rxjava: 2.2.0!!" )}Copy the code

Although RxAndroid :2.1.1 indirectly relies on RxJava :2.2.6, rxJava :2.2.0!! Strict constraints exist, the final resolution is RxJava :2.2.0, in addition to support range constraints, such as [2.0.0, 2.2.0]!! And another point that Strictly cannot degrade directly dependent component versions compared to isForce.

  • Prefer → a version is preferred when there is no stronger constraint;
  • Reject → Generally used to reject a version, such as a version with a specific bug;

References:

  • Mastering Gradle
  • Gradle best practices
  • Declaring dependencies
  • Understanding the serial | gradle frame # 2: dependence analysis
  • Alibaba Cloud Effect Maven

This article is part of the “Gold Nuggets For Free!” Event, click to view details of the event