Gradle Build Variants

You may think that the directory structure and files involved in an Android Studio project is a bit complicated. In truth, what you have seen so far is actually fairly simple, when you start to consider the real world of product development. Simple apps can get by with simple structures like we have seen so far, but:

In this chapter, we will explore the concept of “build variants” in Gradle and Android Studio and how you can take advantage of them for your projects.

Prerequisites

Understanding this chapter requires that you have read the chapters the core chapters of the book.

Objectives of the Project Structure

In the beginning, Android apps tended to be pretty simple, as we only had a handful of devices, a smattering of users, one primary distribution channel (the then-Android Market) and few major investors in the Android ecosystem.

Times have changed.

Now, Android apps for public consumption can be terribly complex, let alone apps for internal enterprise use (which seem to be complex as a side effect of being developed by an enterprise). We have multiple distribution channels, such as the Amazon AppStore for Android and Yandex.Store. We have a billion devices and nearly a billion users. Brands large and small are flocking to Android, bringing with them their own challenges.

The new build system is designed to simplify creating complex Android applications, while, ideally, not making simple Android applications a lot harder. It is designed for scenarios like:

The new project structure, coupled with the Android Gradle Plugin and Gradle itself, makes all of this possible… albeit with a bit of a learning curve.

Terminology

To understand what the new project structure entails, we need to define a few terms, from Gradle and the Android Gradle Plugin.

Source Sets

To quote the Gradle documentation: “A source set is simply a group of source files which are compiled and executed together.” Here, “source” means all the inputs that you are creating for the app, such as Java source code, Android resources and manifest files, and the like. This is in contrast to dependencies, which are inputs that you are (usually) obtaining from other developers, such as reusable libraries.

source sets, on their own, have no particular semantic meaning. You can elect to have your project use a single source set, or several source sets, for organizing your code. You might have different source sets for:

As we will see, the new project structure assumes the existence of at least one source set, typically named main, but other features of the new build system will involve additional source sets.

Build Types

A build type is one axis of possible alternative outcomes from the build process.

By default, the Android Gradle Plugin assumes that you want two build types: release and debug. These may go through somewhat different build steps, such as applying ProGuard to the release build but skipping it for the debug build.

The Android Gradle Plugin though allows build types to have slightly different configurations, such as adding a .debug suffix to the APK’s package name, so that you can have a release build and a debug build of your app on the same device at the same time. You also can create new build types for handling different scenarios. The new build system documentation, for example, suggests a separate “jnidebug” build type, where you can indicate that the Linux .so files for a project should be compiled in debug mode.

As we will see, creating a new build type involves modifications to the build.gradle file and adding a matching source set.

Product Flavors

A build type is one axis for varying your output. A product flavor is another, independent axis for varying your output.

Product flavors are designed for scenarios where you want different release output for different cases. For example, you may want to have one version of your app built to use Google’s in-app purchasing APIs (for distribution through the Play Store) and another version of your app built to use Amazon’s in-app purchasing APIs (for distribution through the Amazon AppStore for Android). In this case, both versions of the app will be available in release form, and you may wish to have separate debug builds as well. And most of the code for the two versions of the app will be the same. However, you will have different code for the different distribution channels — not only does the right code have to run for the right channel, but there is no particular value in distributing the code for one channel through the other channel.

Another example would be an app that is branded and configured for different enterprise customers. You see this a lot with Web apps — the vendor sells a branded-and-configured version of the Web app to the customer, whether that app runs on vendor-supplied hardware or customer-supplied hardware. Similarly, the maker of an Android app for collecting employee timesheets might want to offer to its customers for their version of the timesheet app to sport the customer’s logo, tie into the customer’s specific accounting server, enable or disable features based upon how the customer uses timesheets, and so on. However, most of the code is shared between all customers, and so when the app is updated to add features or fix bugs, new builds are needed for all of the customers. In this case, each customer can be set up as an independent product flavor, sharing much of the code, but with slightly different code, resources (e.g., logo), and configuration based upon that customer’s needs.

Product flavors are optional. If you do not describe any product flavors in your build.gradle file, it is assumed that you have a single product flavor, referred to internally as default. Many apps will not need product flavors; this is a feature that you will opt into as needed.

As we will see, creating a new product flavor involves modifications to the build.gradle file and (usually) adding a matching source set.

Build Variants

The term “build variant” is used for the cross product of the build types and the product flavors. So, a project with debug and release build types and google and amazon product flavors will result in a total of four build variants by default:

  1. debug + google
  2. debug + amazon
  3. release + google
  4. release + amazon

Flavor Dimensions

Sometimes, even this is insufficient flexibility, such as the google and amazon scenario described earlier in this section. Or, you might need separate free versus paid editions, if you want to have an up-front fee for accessing a premium version of your app.

Product flavors are considered to be part of a “flavor dimension”. All flavors have to be a part of a flavor dimension, starting with Android Studio 3.0 — previously, they could be part of a default flavor dimension. You can have as many flavor dimensions as you need (e.g., one for free versus paid, one for distribution channel).

These then add another factor into the cross-product that determines your build variants. Suppose we have a dist flavor dimension, consisting of free and paid product flavors, and we have a channel flavor dimension, consisting of google and amazon flavors. Now, we have a total of 8 possible build variants, when we factor in the build types:

  1. debug + google + free
  2. debug + amazon + free
  3. release + google + free
  4. release + amazon + free
  5. debug + google + paid
  6. debug + amazon + paid
  7. release + google + paid
  8. release + amazon + paid

Configuring the Stock Build Types

The debug and release build types are ready “out of the box” for your use, with a reasonable set of defaults. However, you can change those defaults and make other adjustments to how those build types work, in addition to defining your own build types. Here, we will look at the options for changing the behavior of any build type, focusing on the stock debug and release build types.

Source Sets

Each build type can have its own associated source set. If you skip the directory for it, that means that the build type is not contributing changes to the main source set.

For example, later in the book, we will take a look at Stetho, a diagnostic library that you can add to your Android app to help understand what is going on inside of it. This code is handy during development, but it is not the sort of thing that you want to be shipping in production. Instead, you want to use it only for debug builds.

So, in the Diagnostics/Stetho sample project, we have not only a main source set, but also a debug source set alongside main. In debug, we have an AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.commonsware.android.stetho"
  android:versionCode="1"
  android:versionName="1.0">

  <application
    android:name="com.commonsware.android.stetho.StethoApp"
    tools:replace="android:name">
  </application>

</manifest>
(from Diagnostics/Stetho/app/src/debug/AndroidManifest.xml)

The com.commonsware.android.stetho.StethoApp Java class referenced in the <application> element also resides in the debug source set.

We will see how these blend in with the contents of our main source set later in this chapter.

build.gradle Settings

We can also use the buildTypes closure in build.gradle to configure the behavior of the debug and/or release build types.

For example, a very popular thing to use is applicationIdSuffix, to append a value onto the application ID for builds of a particular build type. This allows the application ID for that build type to be different than the application ID of other build types of the same app. In other words, you can have debug and release builds installed on the same device at the same time.

As was noted earlier in the book, the defaultConfig closure allows us to change aspects of what is found in the AndroidManifest.xml file, replacing anything found in the actual file from the main source set.

Similarly, the buildTypes closure is where we configure the behavior of the build types. Each build type to be configured gets its own closure inside of buildTypes, and in there we can override various properties.

Notable properties that we can specify for a build type include:

So, we can have:


buildTypes {
    debug {
        applicationIdSuffix ".d"
    }
}

Our debug build type adds suffixes to the application ID role for the package name. Note that altering the application ID only affects the package name as seen by Android when the app is installed and when the app is run. It does not affect the directory in which the R class is built, which uses the package name from the AndroidManifest.xml file. It also does not affect any of the Java packages for our own classes, which are whatever we used when we wrote them. Hence, much of our code will be oblivious to the package name change. However, if you want to reference the real package name, such as for looking things up in PackageManager or for use with constructing a ComponentName, use getPackageName() on any Context (like an Activity), rather than some hard-coded string, as getPackageName() returns what the runtime environment thinks the package is, which will include any suffixes added during the build process. Or, use BuildConfig.APPLICATION_ID, in cases where you do not have a Context handy on which to call getPackageName().

We can also have a signingConfig property, for configuring how our APK files are digitally signed. This will be covered in a later chapter.

Order of Precedence

Properties defined for a build type, and the properties defined for the defaultConfig will override their equivalents in the AndroidManifest.xml file. However, a build type’s source set can also have its own AndroidManifest.xml file. The overall order of precedence is:

However, merging manifests in general is a complex topic, with a separate chapter later in this book.

Resources from the build type’s source set are merged into the resources from the main source set, and if there are collisions, the build type’s resource takes precedence. The same is true for assets.

However, the behavior of Java source is slightly different. The build type’s source set is still merged with the main source set, but if there is a collision, the result is a build error. Either the build type or the main source set can define any given source file, not both. So, while debug could have one version of your/package/name/Foo.java and release could have a different version of your/package/name/Foo.java, main could not also have your/package/name/Foo.java. Hence, if you define a class in a build type, you may need to define that class in all build types, so that any references from main to that class are satisfied for all build types.

One case where this would not be required would be for debug-only activities. Suppose that you wanted an activity in your app to provide diagnostic information to developers of that app regarding the state of caches and other in-memory constructs. While you could get at that stuff via a debugger, that is sometimes annoying, and just tapping on a launcher icon can be easier. But you do not want, let alone need, this diagnostic activity in your release builds. To make this work, you would put the activity’s Java class only in the debug source set, along with its resources and manifest entry (complete with MAIN/LAUNCHER <intent-filter>). Since the main source set does not refer to your diagnostic activity, there is no requirement for the release build type to have an implementation of that Java class.

Adding Build Types

Many developers will fare just fine with the debug and release build types, perhaps with some adjustments as shown above. A few developers, though, will have other scenarios that warrant new build types. Fortunately, adding a new build type is rather easy.

First, pick whether you want to have the new build type start with the settings from debug or release. Then, use initWith to create a new build type initialized from that existing build type:


    buildTypes {
        debug {
          applicationIdSuffix ".d"
          versionNameSuffix "-debug"
        }

        dogfood.initWith(buildTypes.debug)

        dogfood {
            applicationIdSuffix ".dawg"
        }
    }

Here, we define a new dogfood build type, with its own application ID suffix.

As with the built-in build types, your new build types can have their own source sets, by adding the appropriately-named directories underneath src/.

Now, we gain Gradle tasks with Dogfood in the name, like installDogfood, to go along with their Debug and Release counterparts.

Adding Product Flavors and Getting Build Variants

Many apps will not need product flavors, but some will. Adding a product flavor is similar, in many respects, to adding a build type.

This involves two steps:

  1. Having a flavorDimensions statement in the android closure, to define the flavor dimension(s) that you wish to use
  2. Having a productFlavors closure where you define the flavors and their properties, including what dimension they belong to


flavorDimensions "channel"

productFlavors {
    goog {
        dimension "channel"
        applicationId "com.commonsware.android.awesomeapp.goog"
    }

    amzn {
        dimension "channel"
        applicationId "com.commonsware.android.awesomeapp.amzn"
    }
}

Here, we define one flavor dimension (channel) and two product flavors (goog and amzn), assigning a distinct applicationId to each of those flavors.

The defaultConfig is implemented using the same object type as is used for product flavors. Hence, we can configure the same things on a product flavor that we can on the defaultConfig, such as applicationId, as is done in this build.gradle file.

We could have source sets for each of these as well. For example, the goog source set might contain code that is used only for Google Play ecosystem devices (e.g., Play Services), while amzn might contain code used for devices distributed by other channels.

In terms of order of precedence:

Our task names get more numerous and more complicated, to reflect the cross product of the product flavors and build types. Now, rather than installDebug, and installRelease, we have:

Doing the Splits

The Android Gradle Plugin offers splits as a lightweight canned replacement for product flavors for two scenarios:

All you as a developer do is request that a particular split be enabled, with limited configuration. Notably, you do not have separate Gradle configuration (e.g., applicationId) nor source sets for splits. That allows splits to be processed more quickly at build time, as the build tools can make some simplifying assumptions and avoid a lot of recompiling.

Scoping Your Splits

A split, by default, will generate one APK per possible type of output. For example, splitting on density will give you one APK for ldpi, mdpi, tvdpi, hdpi, xhdpi, xxhdpi, and xxxhdpi. Plus, in the case of density, you also get one “universal” APK containing support for all densities by default.

That’s nice… but what if you do not need separate APKs for all of those densities? For example, if you do not ship tvdpi resources, there is little reason to set up an APK for it separate from, say, the hdpi APK.

There are two basic patterns to controlling the scope of what gets built:

  1. Use an exclude statement to start with the defaults and remove some options
  2. Use a reset() method to wipe out the defaults, then use an include statement to list what you want

In other words, exclude implements a blacklist, and the reset()/include combination implements a whitelist. All else being equal, a whitelist is probably a better choice, so you can explicitly line it up with what you have written in your app.

Requesting NDK Splits

In your android closure, you can add a splits closure, containing an abi closure, which in turn sets up the APK splits by CPU architecture:


splits {
  abi {
    enable true
    reset()
    include 'armeabi-v7a', 'x86'
    universalApk true
  }
}

Here, we:

The latter would be useful for distribution channels that do not allow you to upload multiple APK files for different CPU architectures. This way, you can at least distribute your app there, even if it takes up more disk space than you like. By default, for the CPU architectures, you do not get a “universal APK”.

Requesting Density Splits

The same basic pattern can be implemented for densities:


splits {
  density {
    enable true
    reset()
    include 'hdpi', 'xhdpi', 'xxhdpi'
  }
}

Once again, we enable the split, reset the defaults, then opt into the densities that we want.

Note, though, that a “universal APK” is always generated for densities. We do not need to have universalApk true, and it would appear that universalApk false is not an option at the present time.

Gradle and Android Studio

While most of the work involving build variants revolves around the module’s build.gradle file, there are some things to consider when working with build variants within Android Studio.

The Build Variants View

When we run our project, Android Studio does not prompt us for a build type or a product flavor. It just runs the project. This begs the question of how Android Studio is determining which build variant is the one to run.

This is handled by the Build Variants view, usually docked on the left side of the Android Studio IDE window:

Build Variants View, for a Simple Project
Figure 314: Build Variants View, for a Simple Project

Each of your app’s modules is shown, along with the current build variant that will be used if you run that module. Tapping on the build variant will allow you to choose an alternative build variant:

Build Variants View, for a Project with Custom Build Types and Product Flavors
Figure 315: Build Variants View, for a Project with Custom Build Types and Product Flavors

The Android Project View

Earlier in the book, when introducing Android Studio, we saw the Android project view. Elsewhere, we saw how the Android project view can help you manage resources across multiple resource sets.

Just as the Android project view “collapses” resource set, it also collapses source sets:

Android Project View, Showing Java Source
Figure 316: Android Project View, Showing Java Source

Here, we have two editions of the com.commonsware.myapplication package. One is just the package name, while the other has “(androidTest)” appended to it. That, as you might imagine, reflects the main and androidTest source set, respectively:

Classic Project View, Showing Java Source
Figure 317: Classic Project View, Showing Java Source

This may be a bit useful between main and androidTest. It is likely to be far more useful if you employ product flavors, as your classes for the flavors will appear side-by-side… at least for the currently-selected flavor in the Build Variants view.

Flavors, Build Types, and the Project Structure Dialog

You are welcome to use the Build Types and Product Flavors tabs in the project structure dialog to maintain these portions of your build.gradle file, at least for simpler scenarios.