Managing Android Build Variants With Caveman

Tim Mutton
5 min readMar 11, 2016

--

When developing Android applications you often want to be able to build different versions of the same application from a single project. For example, you may want to build a version for the QA team that uses a different Google Analytics key, or a version for user acceptance testing that uses your staging backend but disables logging. This post will discuss how you can achieve this using Gradle build variants, as well as discuss a new alternative.

What are build variants?

Build variants are a combination of a product flavor and a build type. A product flavor is a different edition of your application, for example QA, UAT, staging and production, whereas a build type is an environment setting for your application, for example the default debug and release build types that are used in Gradle.

For each build type and product flavor, Gradle allows you to set build configuration fields which are compiled into static fields on a BuildConfig class. In your build script, this takes the form of:

with the resulting BuildConfig looking like:

What are the drawbacks to build variants?

The current system of managing build configurations is pretty good, especially if your configurations are pretty stable, however there are some issues I’ve identified.

Since these values are compiled into the application itself this means that if you want to create or modify a build configuration then you need to update the build script and recompile the application. This is particularly problematic if testers, project managers etc want to do this, as it means they need to rely on a developer to make this change for them, which in turn means that even a simple change can take a while.

This system also means that you will have multiple versions of your application which you need to keep in sync. This pain can be mostly managed by your CI system, but you are still responsible for ensuring that all versions installed on the device are up to date.

Finally, the arguments to buildConfigField are all strings. This is fine for the key, but defining the type and value as a string is clunky and error prone. This issue can be somewhat mitigated by defining constants for the types and keys, however the values will still need to be strings that are then cast to their correct type.

Introducing Caveman

Caveman is a tool recently open-sourced by Outware that aims to solve a lot of the aforementioned problems. Caveman comes in two parts, the first being an application that allows you to create and manage your build configurations, which is designed to run alongside your primary app, and the second being a library that pulls data from the Caveman application (via a content provider), which defaults to production values if no Caveman application is found.

Caveman Application

Configuring the Caveman application is simple. The Caveman application is designed so that you can add it as a module of your current project, either as a git submodule or by copying it in.

Once you’ve added the Caveman application to your project, you can configure what build configuration properties your project requires via res/raw/configurations.json. This takes the form of:

For every property you must have a key, a human readable name and a default value for new environments. Unlike buildConfigFields, this solution allows you to use the number, boolean and string JSON data types for your default values, which in turn sets the type on the resulting Java Object. The Caveman application also lets you pre-fill environments using res/raw/environments.json, which takes the form of:

The Caveman application then uses this information to generate a list of environments that is presented upon installation.

From here you can add, edit or delete environments using the buttons at the bottom of the screen, with the selected environment being the one that is provided to your primary application. If you set your environment to “Production”, which you may have noticed above had no preset properties, the primary application will use its built-in production values.

Using Caveman In Your Primary Application

Caveman also comes with a library, which is hosted on jCenter, that makes it easy to retrieve the values from the Caveman application, or use a default value if it is not present. Once you set this up, retrieving the required value is as easy as making one call to the Environment. A full example of this, using the above configuration and environments, would look like:

Security

Communication between the Caveman application and primary application is handled by a ContentProvider with signature level protection, which means that both the Caveman application and primary application are required to be signed by the same certificate for data to be transferred.

Due to the design of Caveman, the production data should only exist on the primary application and will not be sent between processes. Furthermore, the Caveman application should only exist on test devices.

Wrapping Up

Hopefully by now the benefits of using Caveman should be obvious, but to reiterate some of the above points the main benefits of using Caveman over buildConfigFields are:

  • Adding, changing or removing environments doesn’t require recompiling your application
  • Changes can be made by anyone, not just a developer
  • You only need to build one version of your application, instead of one per configuration
  • The type of the field is determined by the type of the default value, not by supplying the variable type as a string

The full source code to Caveman, including instructions on how to set it up as well as a sample application, can be found on Github. Any feedback, pull requests etc would be greatly appreciated.

--

--