Advanced Gradle for Android Tips

There are lots of things you can do given a full scripting language as the basis for your build system. This chapter represents a collection of tips for things that you can do that go beyond stock capabilities provided by the Android Gradle Plugin.

Prerequisites

Understanding this chapter requires that you have read the chapters that introduce Gradle and cover basic Gradle/Android integration, including the project structure.

Gradle, DRY

Ideally, your build scripts do not repeat themselves any more than is logically necessary. For example, a project and sub-projects probably should use the same version of the build tools, yet by default, we define them in each build.gradle file. This section outlines some ways to consolidate this sort of configuration.

It’s build.gradle All The Way Down

If you have sub-projects, you can have build.gradle files at each level of your project hierarchy. Your top-level build.gradle file is also applied to the sub-projects when they are built.

In particular, you can “pass data” from the top-level build.gradle file to sub-projects by configuring the ext object via a closure. In the top-level build.gradle file, you would put common values to be used:


ext {
   compileSdkVersion=26
}

(note the use of the = sign here)

Sub-projects can then reference rootProject.ext to retrieve those values:


android {
    compileSdkVersion rootProject.ext.compileSdkVersion
}

By this means, you can ensure that whatever needs to be synchronized at build time is synchronized, by defining it once.

Another way that a top-level build.gradle file can configure subprojects is via the subprojects closure. This contains bits of configuration that will be applied to each of the subprojects as a part of their builds.

Note that subprojects applies to all sub-projects (a.k.a., modules), which limits its utility. For example, a top-level project with one sub-project for an app and another sub-project for a library used by that app cannot readily use subprojects. That is because the library sub-project needs to configure the com.android.library plugin, while the application sub-project needs to configure the com.android.application plugin. The subprojects closure is only good for common configuration to apply to all sub-projects regardless of project type.

gradle.properties

Another approach would be to add a gradle.properties file to your project root directory. Those properties are automatically read in and would be available up and down your project hierarchy.

Per-developer properties can go in a gradle.properties file in the user’s Gradle home directory (e.g., ~/.gradle on Linux), where they will not be accidentally checked into version control.

So, to achieve the synchronized compileSdkVersion value, you could have a gradle.properties file with:


COMPILE_SDK_VERSION=26

Then, your projects’ build.gradle files could use:


android {
    compileSdkVersion COMPILE_SDK_VERSION
}

Custom Properties Files

You are also welcome to use your own custom properties files. For example, perhaps you want to use gradle.properties for properties that you are willing to put in version control (e.g., BUILD_TOOLS_VERSION), but you would also like to use a properties file to keep your code-signing details outside of your build.gradle file and out of version control.

Loading in custom properties files is slightly clunky, as it does not appear to be built into Gradle itself. However, you can take advantage of the fact that Gradle is backed by Groovy and use some ordinary Groovy code to load the properties.


def keystorePropertiesFile = file('keystore.properties')
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

These three lines create a java.util.Properties object from a keystore.properties file located in the module’s directory. Individual properties can be read using <> syntax, the same as Groovy uses for a Map:


signingConfigs {
  release {
    keyAlias keystoreProperties['keyAlias']
    keyPassword keystoreProperties['keyPassword']
    storeFile file(keystoreProperties['storeFile'])
    storePassword keystoreProperties['storePassword']
  }
}

This fills in the signing configuration from the keystore.properties values, assuming that the keystore.properties file itself has the appropriate properties:


storePassword=81016168
keyPassword=9f353470
keyAlias=foo
storeFile=release.jks

Now, your signing information is not in build.gradle, and you can keep a fake keystore.properties in version control (where the properties have invalid values), enough to allow Gradle to process the build.gradle file but not allow for app signing. The machine designated for doing official builds would have the real keystore.properties file and associated keystore.

Environment Variables

Any environment variables with a prefix of ORG_GRADLE_PROJECT_ will show up as global variables in your Gradle script. So, for example, you can access an environment variable named ORG_GRADLE_PROJECT_foo by accessing a foo variable in build.gradle.

If you would prefer to use environment variables without that prefix, you can call System.getenv(), passing in the name of the environment variable, to retrieve its value.

Note, however, that you may or may not have access to the environment variables that you think you should. Android Studio, for example, does not expose environment variables to Gradle for its builds, and so an environment variable that you can access perfectly well from the command line may not be available in the same build.gradle script when run from Android Studio.

Automating APK Version Information

Once the Android Gradle Plugin started catching on, one of the first things many developers raced to do was automate the android:versionCode and android:versionName properties from the manifest. Since those can be defined in a Gradle file (overriding values from any AndroidManifest.xml files), and since Gradle is backed by Groovy, it is possible to programmatically assign values to those properties.

This section outlines a few approaches to that problem.

Auto-Incrementing the versionCode

Since the android:versionCode is a monotonically increasing integer, one approach for automating it is to simply increment it on each build. While this may seem wasteful, two billion builds is a lot of builds, so a solo developer is unlikely to run out. Synchronizing such versionCode values across a team will get a bit more complex, but for an individual case (developer, build server, etc.), it is eminently doable using Groovy.

The Gradle/Versioning sample project uses a version.properties file as the backing store for the version information:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'

    def versionPropsFile = file('version.properties')
    def Properties versionProps = new Properties()

    if (versionPropsFile.canRead()) {
        versionProps.load(new FileInputStream(versionPropsFile))
    }
    else {
        versionProps['VERSION_CODE']='0'
    }

    def code = versionProps['VERSION_CODE'].toInteger() + 1

    versionProps['VERSION_CODE']=code.toString()
    versionProps.store(versionPropsFile.newWriter(), null)

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 26
        versionCode code
        versionName "1.1"
    }
}
(from Gradle/Versioning/app/build.gradle)

First, we try to open a version.properties file. If we find it, we read it in. Otherwise, we initialize our java.util.Properties object with a VERSION_CODE of 0, simulating a starting point.

The script then increments the old value by 1 to get the new code to use. The revised code is then written back to the properties file before it is applied in the defaultConfig closure.

The result is that our versionCode is automatically incremented, and you can see the value change in the version.properties file that is generated into the app/ module directory:

#Sat Nov 04 12:44:54 EDT 2017
VERSION_CODE=3
(from Gradle/Versioning/app/version.properties)

Adding to BuildConfig

The Android development tools have been code-generating the BuildConfig class for some time now. Historically, the sole element of that class was the DEBUG flag, which is true for a debug build and false otherwise. This is useful for doing runtime changes based upon build type, such as only configuring StrictMode in debug builds.

Nowadays, the Android Gradle Plugin also defines things like:

However, you can add your own data members to BuildConfig, by including a buildConfigField statement in the defaultConfig closure of your android closure:


android {
  defaultConfig {
    buildConfigField "int", "FOO", '5'
  }
}

You can use this to embed any sort of information you want into BuildConfig, so long as it is knowable at compile time.

Moreover, you can also have buildConfigField statements in build types. This would be useful if you have custom build types, beyond just debug and release, and you need runtime configuration for those. For example, you could put server URLs in buildConfigField, so your debug server is different from your integration test server, which in turn is different than your production server.

For example, in the chapter on SSL, we will see a sample app for demonstrating network security configuration, a way that apps can restrict their Internet access (e.g., require SSL for everything) and support custom SSL certificates (e.g., self-signed certificates for an internal test server). The Internet/CA sample project that we will see there has a long list of product flavors, and each of them defines a particular server URL to test against:

apply plugin: 'com.android.application'

def WARES='"https://wares.commonsware.com/excerpt-7p0.pdf"'
def SELFSIGNED='"https://scrap.commonsware.com:3001/excerpt-7p0.pdf"'

android {
    compileSdkVersion 27
    buildToolsVersion '27.0.3'

    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 27
    }

    flavorDimensions "default"

    productFlavors {
        comodo {
            dimension "default"
            resValue "string", "app_name", "CA Validation Demo"
            applicationId "com.commonsware.android.downloader.ca.comodo"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_comodo']
            buildConfigField "String", "URL", WARES
        }
        verisign {
            dimension "default"
            resValue "string", "app_name", "Invalid CA Validation Demo"
            applicationId "com.commonsware.android.downloader.ca.verisign"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_verisign']
            buildConfigField "String", "URL", WARES
        }
        system {
            dimension "default"
            resValue "string", "app_name", "System CA Validation Demo"
            applicationId "com.commonsware.android.downloader.ca.system"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_verisign_system']
            buildConfigField "String", "URL", WARES
        }
        pin {
            dimension "default"
            resValue "string", "app_name", "Cert Pin Demo"
            applicationId "com.commonsware.android.downloader.ca.pin"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_pin']
            buildConfigField "String", "URL", WARES
        }
        invalidPin {
            dimension "default"
            resValue "string", "app_name", "Cert Pin Demo"
            applicationId "com.commonsware.android.downloader.ca.invalidpin"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_invalid_pin']
            buildConfigField "String", "URL", WARES
        }
        selfSigned {
            dimension "default"
            resValue "string", "app_name", "Self-Signed Demo"
            applicationId "com.commonsware.android.downloader.ca.ss"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_selfsigned']
            buildConfigField "String", "URL", SELFSIGNED
        }
        override {
            dimension "default"
            resValue "string", "app_name", "Debug Override Demo"
            applicationId "com.commonsware.android.downloader.ca.debug"
            manifestPlaceholders=
                    [networkSecurityConfig: 'network_override']
            buildConfigField "String", "URL", SELFSIGNED
        }
    }
}

repositories {
    maven {
        url "https://s3.amazonaws.com/repo.commonsware.com"
    }
}

dependencies {
    implementation 'com.android.support:support-v13:27.0.2'
    implementation 'com.commonsware.cwac:provider:0.5.3'
    implementation 'com.commonsware.cwac:netsecurity:0.4.4'
}
(from Internet/CA/app/build.gradle)

In this case, to reduce repetition, the two possible values are defined as globals and are poured into BuildConfig.URL via the buildConfigField statements.