Gradle is a topic that every Android student can’t escape.

Do you see other people’s Gradle files clean and clean? And their own is a mess 🏷

Don’t be afraid. In this article, I will share some common operations based on my daily development experience. I hope it can help students like me who don’t know how to [play]Gradle.

Template code extraction

This is the most basic operation. For a normal Model. gradle, the default configuration is as follows:

Wouldn’t it be troublesome if we wrote every model like this, so let’s extract the generic code:

Optimization steps

Create a new gradle file and name it XXX. Gradle. Copy the configuration from the above model and place it in your project.

This is a player model

// This is the default gradle file we just created.
// Note: If your default.gradle is in the project directory, use the.. /, if only in app, please use./
apply from: ".. /default.gradle"
import xxx.*

android {
  	// Resource files used to isolate different models
    resourcePrefix "lc_play_"
}


dependencies {
    compileOnly project(path: ':common')
    api xxx
}
Copy the code

Android {} dependencies{}

Everything inside is superimposed on default.gradle, with unique key-value pairs being replaced.

Define a uniform config configuration

How do you write your version number and other defaults in a project?

For a new project, the default configuration is as follows. Each time you create a new Model, you also need to define its default parameters. If you change the model directly, then if the version changes, it means we need to change it several times, which is not what we want to see.

Optimization steps

Create a new config.gradle with the following content:

// Save some configuration files

// Use git commit records as versionCode
static def gitVersionCode() {
    def cmd = 'git rev-list HEAD --count'
    return cmd.execute().text.trim().toInteger()
}

static def releaseBuildTime() {
    return new Date().format("yyyy.MM.dd", TimeZone.getTimeZone("UTC"))
}

ext {
    android = [compileSdkVersion: 30.applicationId : "com.xxx.xxx".minSdkVersion : 21.targetSdkVersion : 30.buildToolsVersion: "30.0.2".buildTime : releaseBuildTime(),
               versionCode : gitVersionCode(),
               versionName : "1.x.x"]}Copy the code

When using:

android {
    def android = rootProject.ext.android
    defaultConfig {
        multiDexEnabled true
        minSdk android.minSdkVersion
        compileSdk android.compileSdkVersion
        targetSdk android.targetSdkVersion
        versionCode android.versionCode
        versionName android.versionName
    }
 }
Copy the code

Configure your build

Configure different build types

In development, we usually have multiple environments, such as development environment, test environment, online environment:

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }

    dev{
        // initWith stands for allowing copying from other build types and then configuring the Settings we want to change
      	// This represents copying build configuration from release
        initWith release
        // List placeholders
        manifestPlaceholders = [hostName:"com.petterp.testgradle.dev"]
        // Add.test after your package name
        applicationIdSuffix ".dev"}}Copy the code

Dev is our new build type. After the new build type is added, we can use the following matching command in the command line, or click on the far right of As, gradle icon, and select app(depending on the configuration location of our build, the default is app-model). Select Other and you can see the following commands:

You can also use the following command line to execute the Jenkins or CI build:

gradlew buildDev
gradlew assembleDev
Copy the code

Note that for MAC it starts with Gradlew, for Windows it may start with./gradlew


Configuration variations

Generally speaking, if it is only multi-channel, we can choose to use the third-party Walle. If we may have more elaborate Settings, for example, we may have different default configurations for this build type. Such as configuring different Applicationids, resources.

As follows:

If there is only one variant name, all variants will be used automatically. If there are two or more variants, they need to be specified in the variant name, and the variants need to match the group. // Flavor name, similar to style, meaning group. FlavorDimensions "channel" // flavorDimensions ("channel"," API ") productFlavors {demo1 {// Every flavor must have a flavor, The value of flavorDimensions(only if it is a single) is used by default, otherwise an error will be reported if it is not provided. Dimension "channel" // appid suffix, which will override our build type's applicationIdSuffix applicationIdSuffix ".demo" // versionNameSuffix "-demo" } demo2 { dimension "channel" applicationIdSuffix ".demo2" versionNameSuffix "-demo2" } }Copy the code

Then check out our Build Variants:

Gradle automatically creates multiple build variants based on our variant and build type, naming them as the variant name – Build type name.

We can also replace all of the defaults set in the Build type when we configure the variants. The default defaultConfig configuration when we add the build type is the ProductFlavors class, so we can replace all of the defaults in any of the variants.


Combine multiple variants

In some cases, we may want to combine multiple product variants. For example, we want to add a variant of API30, and for this variant, we want demo1 and demo2 to be combined with the corresponding package under API30 when channel is Demo1.

As an example, let’s change the configuration above:

  flavorDimensions("channel"."api")
    productFlavors {
        demo1 {
            dimension "channel"
            applicationIdSuffix ".demo"
            versionNameSuffix "-demo"
        }
        demo2 {
            dimension "channel"
            applicationIdSuffix ".demo2"
            versionNameSuffix "-demo2"
        }
        minApi23 {
            dimension "api"
            minSdk 23
            applicationIdSuffix ".minapi23"
            versionNameSuffix "-minapi23"}}Copy the code

The final result is as follows, the left side is the build variant generated by Gralde, and the right side is the product information after demo1MinApi23Debug is packed:

So we can sum it up as:

Finally, when we package, our package name and version name are generated from a mix of variants, as shown in the figure above, and then we use the configuration with both, with the variant configuration starting with the first as the baseline when the configuration is duplicated.

For example, if we configure the minimum SDK version 21 for the Demo1 variant, then the final package minSdk will also be 21, not minSdk configuration in minApi23. This should be noted.


disambiguation

So how do you choose between variants and build types? They seem to be very similar, right?

It is not difficult to understand, as follows:

For example, if you add a variant of firDev, the following build command is generated by default

FirDevDebug firDevRelase firDevXXX(XXX is your custom build type)Copy the code

Note that debug and relase exist by default, we can override them, otherwise they will remain in default even if removed

Gradle will eventually generate commands for each build type for each variant. Variants are channels, and build types are channels for which there are multiple environments, such as Debug,relase, and more build types that you can customize.

  • So if your scenario only needs to correspond to several different environments, you can configure the build type directly.
  • If you might want to differentiate between dependencies or resource configurations under different packages, configure variations.

Filter the variant

Gradle creates a build variant for every possible combination of all the variants and build types we configure. Of course, there are some variants that we don’t need, so we can create a variant filter in the build.gradle of the corresponding module to remove some unwanted variants.

android{
	...
	variantFilter { variant ->
        def names = variant.flavors*.name
        if (names.contains("demo2")) {
            setIgnore(true)}}... }Copy the code

The effect is as follows:


Configure dependencies for variants

We can also make different dependencies for these variants. Such as:

 demo1Implementation  xxx
 minApi23Implementation xxxx
Copy the code

Common techniques

About Dependency Management

For some environments, we don’t want to rely on certain libraries or models online, and if it’s a tripartite library, there’s usually a dependent version of Relase.

If it is a local model, it is already referenced, so it is necessary to do null package processing for the online environment, leaving only the corresponding package name and entry, the specific implementation is null.

Restrict dependencies to build type

debugImplementation project(":dev")
releaseImplementation project(":dev_noop")
Copy the code

One thing to note is that when we rely on the default debugImplementation and releaseImplementation, do we rely on them for final packaging, Depending on whether the build type in the build command we use is debug or relase, if we use a custom dev, then neither of the above models will depend on it, which makes sense.

The restriction dependency condition is a variant

Correspondingly, if we want the current dependent library or model to be variant-independent of the build type, we can also use our variant -implementation for the dependency, as follows:

demo1Implementation project(":dev")
Copy the code

This means that if we use demo1’s gradle command when we package, such as assembleDemo1Debug, it will participate in the dependency regardless of whether the current build type is Debug or Release or whatever.

Exclude passed dependencies

In development, we often encounter dependency conflicts. For the dependency conflicts caused by third-party libraries, it is easier to solve them. We just need to use exclude to solve them, as shown below:

dependencies {
    implementation("Androidx. Lifecycle: lifecycle - extensions: 2.2.0." ") {
        exclude group: 'androidx.lifecycle'.module: 'lifecycle-process'}}Copy the code

Unified global dependent versions

Sometimes there are multiple versions of a library. Gradle uses the highest version of a library by default, but it still generates an error.

android{
	defaultConfig {
        configurations.all {
            resolutionStrategy {
                force AndroidX.Core
                force AndroidX.Ktx.Core
                force AndroidX.Work_Runtime
            }
        }
     }
}
Copy the code

Simplify your BuildConfig configuration

In development, it’s common to write configuration information into BuildConfig for use in development, which is one of the most common tools.

Configuration Mode 1

The simplest way is to write our config into the configuration when we run the applicationVariants Task, as shown in the following example:

app/ build.gradle

android.applicationVariants.all { variant ->
    if ("release" == variant.buildType.getName()) {
        variant.buildConfigField "String"."baseUrl"."\"xxx\""
    } else if ("preReleaseDebug" == variant.buildType.getName()) {
        variant.buildConfigField "String"."baseUrl"."\"xxx\""
    } else {
        variant.buildConfigField "String"."baseUrl"."\"xxx\""
    }
    variant.buildConfigField "String"."buglyAppId"."\"xx\""
    variant.buildConfigField "String"."xiaomiAppId"."\"xx\"". }Copy the code

At write time, we can also determine what to write by judging the current build type.

Optimizing the allocation of

If the configuration is very small, the above method can accept, but if the configuration parameters are many, hundreds? And that’s when we need to extract it.

So we can create a new build_config.gradle and copy the above code into it.

Then, in the required module, you can rely on it.

apply from: "build_config.gradle"
Copy the code

The advantage of this is that it reduces the logic in our app-build.gradle and improves efficiency and readability by adding a unified entry point.


Configuration Mode 2

There is another way, which is to define the two methods ourselves and call them in buildType, and write the config configuration to a file to manage accordingly.

Sample code:

app/ build.gradle

buildTypes {
    // Read all configurations under./build_extras
    def configBuildExtras = { com.android.build.gradle.internal.dsl.BuildType type ->
        // This closure reads lines from "build_extras" file and feeds its content to BuildConfig
        // Nothing but a better way of storing magic numbers
        def buildExtras = new FileInputStream(file("./build_extras"))
        buildExtras.eachLine {
            def keyValue = it == null ? null : it.split("- >")
            if(keyValue ! =null && keyValue.length == 2) {
                type.buildConfigField("String", keyValue[0].toUpperCase(), "\"${keyValue[1]}\"") } } } release { ... configBuildExtras(delegate) ... } debug{ ... configBuildExtras(delegate) ... }}Copy the code

build_extras

. baseUrl -> xxx buglyId -> xxx ...Copy the code

We can decide the above two configuration methods according to our own needs. Personally, I prefer method 1, because it looks simpler after all, but in fact, there is not much difference between the two implementation methods, depending on personal habits.

Manage global plug-in dependencies

At some point, all of our models may need to be integrated with a plugin. In this case, we can avoid integrating with each gradle by managing it globally in our build.gradle project:

// Manage global plug-in dependencies
subprojects { subproject ->
    // All subprojects are applied by default
    apply plugin: xxx
    // If you want to apply to a subproject, you can use subproject.name to determine which subproject to apply to
    // subproject.name is the name of your subproject, as shown below
    / / the official documentation address: https://guides.gradle.org/creating-multi-project-builds/#add_documentation
// if (subproject.name == "app") {
// apply plugin: 'com.android.application'
// apply plugin: 'kotlin-android'
// apply plugin: 'kotlin-android-extensions'
/ /}
}
Copy the code

Adjust your component switches dynamically

For some components, dependencies during debug development may have an impact on our compile time, so it would be better if we added a corresponding on/off control:

buildscript {
	ext.enableBooster = flase
	ext.enableBugly = flase

	if (enableBooster)
   	classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
 }
Copy the code

If we had static control every time, we wouldn’t be able to do it when we used CI for packaging. So we can change the logic accordingly:

We create a folder with the corresponding ignore files as follows:

Then we’ll change the corresponding buildScript logic:

buildscript { ext.enableBooster = ! file("ignore/.boosterignore").exists() ext.enableBugly = ! file("ignore/.buglyignore").exists()

	if (enableBooster)
   	classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
 }
Copy the code

The enabling status of the plug-in in CI packaging is determined by determining whether the corresponding file of the plug-in exists. When CI is packaged, we just need to delete the configuration ignore file by shell or run the command by gradle. Since this article is about gradle operations, we will focus on examples of gradle commands.

Define your own Gradle plug-in

Let’s write a simple plug-in for the most basic, used to remove the corresponding files, to achieve the purpose of switching the plug-in.

task checkIgnore {
    println "-- -- -- -- -- -- -- checkIgnore -- -- -- -- -- -- -- -- start -- >"
    removeIgnore("enableBugly".".buglyignore")
    removeIgnore("enableGms".".gmsignore")
    removeIgnore("enableByteTrack".".bytedancetrackerignore")
    removeIgnore("enableSatrack".".satrackerignore")
    removeIgnore("enableBooster".".boosterignore")
    removeIgnore("enableHms".".hmsignore")
    removeIgnore("enablePrivacy".".privacyignore")
    println "-- -- -- -- -- -- -- checkIgnore -- -- -- -- -- -- -- -- end -- >"
}

def removeIgnore(String name, ignoreName) {
    if (project.hasProperty(name)) {
        delete ".. /ignore/$ignoreName"
        def sdkName = name.replaceAll("enable"."")
        println "-------- opened $sdkName" + "Component"}}Copy the code

The plugin is simply used to remove the plugin file from our Gradle command.

gradlew app:assembleRoyalFinalDebug  -PenableBugly=true
Copy the code

In ci-build, we can dynamically decide whether to enable a plug-in or not by passing the corresponding value.


Optimized version

Although the above method is convenient, but it still looks very troublesome, so is it easier, just use Gradle. This problem can be easily solved with a little knowledge of the Gradle lifecycle.

We can listen to gradle’s life cycle in settings.gradle, and then when the project structure is loaded, which is projectsLoaded, we can tell if there is a parameter, then turn it on, otherwise turn it off.

Example:

settings.gradle

gradle.projectsLoaded { proj ->
    println 'projectsLoaded()-> Project structure loaded complete (initialization phase ends) '
    def rootProject = proj.gradle.rootProject
    rootProject.ext.enableBugly = rootProject.findProperty("enableBugly") ?: false
    rootProject.ext.enableBooster = rootProject.findProperty("enableBooster") ?: false
    rootProject.ext.enableGms = rootProject.findProperty("enableGms") ?: false
    rootProject.ext.enableBytedance = rootProject.findProperty("enableBytedance") ?: false
    rootProject.ext.enableSadance = rootProject.findProperty("enableSadance") ?: false
    rootProject.ext.enableHms = rootProject.findProperty("enableHms") ?: false
    rootProject.ext.enablePrivacy = rootProject.findProperty("enablePrivacy") ?: false
}
Copy the code

Run the build command with the following parameters:

gradlew assembleDebug -PenablePrivacy=true 
Copy the code

reference

Android Developer – Configure your build

I’m Petterp, a third-rate developer. If this article helped you, please give it a thumbs up.