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.

8 comments:

  1. You can define multiple source folders (to go along side main). When the build variant is generated, it will pick up the appropriate files.
    I made a config file in 'paid' and 'free' source folders with keys and a boolean to keep the code exactly the same (programmatic check for paid/free).
    You may be able to do the same with the manifest file (one in main, paid and free) which may help with the content provider (I haven't had chance to play with this yet)

    ReplyDelete
    Replies
    1. The way that productFlavor and buildType resource folders get merged into the final set of resources does not work for the problem described in this post. The current system works by adding/merging the given productFlavor and buildType resource folders. The issue is when I need a unique resource for each buildVariant. Take the productFlavor { free, paid} and buildType { alpha, beta} example, for instance. There is no built-in facility for generating/selecting a unique resource for each of the buildVariants: freeAlpha, freeBeta, paidFree, and paidBeta.

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Hello,

      If your code has generated the override value (and the contents of the file contain the correct value), but this override doesn't show up under build/res/all, then it is likely that variant hasn't been told to pick up the override resource folder. In my example, look at the sourceSets closure from build.gradle. Builds of the debug buildType will not take overrides (because it does not specify a resource folder for itself), but all other buildTypes specify a path for res.srcDirs.

      Delete
    2. ok, I think now I understand the problem... I didn't see the sourceSets of you buildTypes. I thought that your extra flavour "unique_overrides" would be merged with the other flavor ("free" and "paid").

      So this solution doesn't work for me.

      I think I have only two options: either I override string values of my flavors or complete lines in my manifest. I also read a lot about replacing tokens in gradle but I couldn't make it work.

      Thanks for your fast reply :)

      Delete
  3. Hey Brad, wow, thanks for this. The valuable "take home" for me is just how the override mechanism works for Android Gradle. I too noticed that my gradle code executed on a simple "gradle tasks" call. I believe the reason is that the code is executing on the 'configuration' rather than 'execute' phase.
    I wonder is it possible to rewrite your override to:
    variant.mergeResources.doFirst {
    overrideMapsKey(variant)
    overrideContentProviderAuthority(variant)
    }
    I intend to play with this, but haven't tried it yet.

    ReplyDelete
    Replies
    1. Yes, dependsOn needs a task or a closure returning a task. doFirst should be the way to go, especially since you don't want to override those things if they won't be used at all (AS calling generateDebugSources for example).

      Delete
  4. This comment has been removed by the author.

    ReplyDelete