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.
Understanding this chapter requires that you have read the chapters that introduce Gradle and cover basic Gradle/Android integration, including the project structure.
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.
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.
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
}
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.
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.
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.
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"
}
}
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
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:
BUILD_TYPE
, which is the build type used to build this APK.FLAVOR
, which is the product flavor used to build this APK.APPLICATION_ID
, which is the name that serves as the application ID
(i.e., it includes build type suffixes and product flavor overrides). This
is useful for cases where you cannot just call getPackageName()
on a
Context
because you do not have a handy Context
.VERSION_CODE
, which is the version code derived from your manifest in
conjunction with any overrides coming from your build.gradle
file.VERSION_NAME
, which is the version name derived from your manifest in
conjunction with any overrides coming from your build.gradle
file.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'
}
In this case, to reduce repetition, the two possible values are defined as
globals and are poured into BuildConfig.URL
via the buildConfigField
statements.