Monday, August 26, 2013

Android Gradle - Building Unique Build Variants

The new build system for Android, based upon Gradle, is a major improvement over the original build system. The introduction of build types and product flavors to create unique applications from the same source code is very powerful. Moreover, Gradle is more pleasurable and easy to use than ant, especially if you execute custom build logic. For these reasons, the new build system has solved most of my build woes.

Unfortunately, many of the APIs were not written with this new, scalable build system in mind. This means that build variants (the product of a buildType and productFlavor) generated from a relatively complex codebase do not yield the expected results. Let's start with a concrete example.

The Problem


Let's say that your application has some pretty standard productFlavors and buildTypes defined in your build.gradle file.
    productFlavors {
        free {
            packageName "com.gradle.build.example.free"
        }

        paid {
            packageName "com.gradle.build.example.paid"
        }
    }

    buildTypes {
        alpha {
            packageNameSuffix ".alpha"
        }

        beta {
            packageNameSuffix ".beta"
        }
    }


Everything is fine and dandy. You are able to assemble 4 APKs from this configuration: freeAlpha, freeBeta, paidAlpha, paidBeta. By specifying packageNameSuffix in the buildType, each of these APKs can be installed alongside each other on a device. This is great for testing and even better if you release both a free and paid version of your app to the Play Store. This is precisely what Gradle Android aims to solve!

Now let's consider the case where your application defines a ContentProvider in the AndroidManifest. If you try to install these APKs, you'll receive the following error:
Installation error: INSTALL_FAILED_CONFLICTING_PROVIDER
As it currently stands, Android Gradle does not have a good solution for this problem. There is a similar problem if you are using Google Maps v2. Google issues API keys that are created based upon your application package name and the signing key. The install doesn't fail, but you have to make sure to pick the correct key otherwise your map will not load. I'm sure the Android API is full of other components that behave similarly. The new build system is great at creating the union of all resources required for a given buildVariant. But when you need to define a unique identifier for a specific buildVariant, there aren't any simple solutions.

A Solution


An approach that I've taken to automating this selection of "unique identifiers" for a given build variant boils down to dynamically generating the override identifiers and writing this to a standard Android resource folder. Then any buildType that would like to take advantage of these unique identifiers can refer to this res directory in its buildType config. Because code speaks louder than words; I've pushed a sample project to: Gradle-Build-Example.

android.applicationVariants.all { variant ->
    variant.mergeResources.dependsOn {
        overrideMapsKey(variant)
        overrideContentProviderAuthority(variant)
    }
}
For the ContentProvider authority strings, I make sure to define these strings as a resource in its own file. I then iterate over the XML tree and append a unique suffix to the end of each String resource and write this to the override resource folder. The best part about this solution is that it is fully automated. If you add a new ContentProvider, just make sure to add it's authority String in the file that gets parsed and overridden. If you add a new buildType or a new productFlavor, it will just work.

For the maps keys, I store them in an associative array in the build.gradle file, keyed off the packageName. The correct maps key will be selected, written to the override file and thus your map tiles will load. I recommend viewing the source code for the full implementation.

Update: The Google Maps v2 key assertion is incorrect. You can assign a set of packageName/signing-key pairs to a given Google Maps v2 key. I would recommend updating your Google maps key to support any new packageName/signing-key pairs you introduce. The example remains, however, for other components that may benefit this strategy.

Solving the problem in this way was done for a few reasons:

1. Scalability. Adding new productFlavors or buildTypes or Android API components that must adhere to this "uniqueness" principle is relatively painless.

2. Automation. Humans aren't required to go in and change the code to replace the Google Maps debug key with the production key when build a release APK, for instance.

3. Intuitive. The overrides all go into one resource folder, so it is easy to see which values are being selected.

Here are some things that I am not so happy about with this solution:

1. Task execution. I think this is mostly because I am still trying to grok Gradle in general, but, using android.applicationVariants.all { variant -> variant..dependsOn myCustomTask} does not seem like the "Gradle" way to run custom code. I've discovered that my customTask gets invoked even when running "./gradlew tasks". This is not what I expect.

2. Location of files. Generating these override files into the src tree is less than ideal. It can be easily mitigated from a dev standpoint by adding this generated file to your .gitignore. It would be preferable if these values could just be added into the standard Gradle build/res folder. This should be possible, I just don't know enough about Gradle Android yet to know how to do something like that.

I hope this post has helped with some of your build problems! Also, I would be very curious to hear from others on how you've solved this problem. And for reference, this sample was built using Android Gradle 0.5.6.

tl;dr: The new Android build system is great. Unfortunately it does not work perfectly. Here is my approach to solving some of its limitations.

Source: https://github.com/bradmcmanus/Gradle-Build-Example

Update: It has come to my attention that Google Maps v2 keys can be associated with a set of packageName/signing key pairs. In lieu of this fact, I would recommend updating your Google Maps v2 key to support any new packageName/signing key pair you introduce. The example remains, however, for other components that may benefit this strategy.