Published
August 2, 2015
Author
Brad McManus
Category
Comments
Time To Read
Estimated reading time: 8 minutes

Easier Maintenance of Android Build Variants

By Brad McManus in Mobile Development on August 2, 2015 |

My name is Brad McManus and I am an Android developer working on Expedia Hotels, Flights & Cars for the Google Play Store. Expedia Inc. owns many travel brands with their own unique applications and requirements. The core functionality of these applications is largely the same and thus it is undesirable to maintain several separate codebases. Building similar applications for our brands from the same codebase saves a lot of engineering time but does require forethought. It can also be difficult in a mature codebase that was not written with this level of configuration in mind.

Extensive configuration and swappable assets can make the process of building several applications simpler, but can also introduce a lot of complexity if not applied appropriately. The Android Gradle build system contains several features that can help development teams manage this complexity. In this post I will share some of the things we’ve learned while building applications for Expedia.

Sharing information is always better with some real code samples, so I’ll include a real-life example and sample project. Suppose you are the Android developer at a fictitious and wildly successful Ecommerce company for pet owners, PetMaster++. Our fictional PetMaster++ offers an unmatched service for owners of every conceivable domesticated animal. PetMaster++ decides to take a multi-branded business approach and rolls out specialized brands that build upon the basic PetMaster++ formula, designed specifically for the highly lucrative high-end dog and cat owner markets.

Product flavors

“Product Flavor” is the key Android Gradle build system concept that allows for built variant configuration. If this concept is new to you, be sure to the official Google documentation of the Android Gradle build system.

Each product flavor can provide its own sources – both Java class files and Android resources like drawables or strings. The product flavor convention allows for structured organization of these flavor-specific source files by storage in separate directories. Flavor sources get compiled into the APK along with the main sources when building the flavor. This means one can easily keep unwanted resources such as large PNGs out of irrelevant builds.

Supporting the cat and dog brands in addition to the generic pets brand from one codebase becomes a first-class concept using the Android Gradle build system by using the productFlavor block in conjunction with the applicationId field:

productFlavors {
    pets {
        applicationId "com.example.petmaster"
    }
    cats {
        applicationId "com.example.cats.only"
    }
    dogs {
        applicationId "com.example.dogs.are.best"
    }
}

Generating these uniquely-branded applications is as simple as invoking the assembly of the specific build variant.

./gradlew assembleCatsDebug
./gradlew assembleDogsDebug

Custom attrs

The build system handles resource overrides and the ability to provide different Java source implementations out of the box, but in the case of a large and complex codebase the build system conventions alone might not be sufficient. In our case we had the desire for more structure. Our team found it crucial to add a convention for UI elements that required re-skinning by defining custom attributes. This was especially important while rolling out new applications by making it easier to assess the work required to customize the application. Each resource that requires a different asset on a product flavor level is backed behind a custom attribute. The steps to define and use a skinnable attribute work as follows:

Declare and define custom attributes.

<!-- Define custom attributes using the `attr` tag in `src/main` -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="skin_noFoodAvailableMessage" format="string" />
</resources>

<!-- src/main Theme -->

<style name="Theme">
    <item name="skin_noFoodAvailableMessage">@string/no_food_available_generic</item>
</style>


<!-- src/dogs Theme -->

<style name="Theme.Dogs">
    <item name="skin_noFoodAvailableMessage">@string/no_food_available_dogs</item>
</style>


<!-- Must define skin_noFoodAvailableMessage for all variants -->

src/main retrieval of dynamically themed resources:

// Client code with a Theme that has defined given custom attr
errorTextView.setText(obtainThemeResId(context, R.attr.skin_noFoodAvailableMessage));

// Context must include Theme information
static int obtainThemeResId(Context context, int attr) {
    TypedArray a = context.obtainStyledAttributes(new int[] { attr });
    if (!a.hasValue(0)) {
        throw new RuntimeException("Theme attribute not defined for attr=" + Integer.toHexString(attr));
    }

    int resId = a.getResourceId(0, -1);
    a.recycle();
    return resId;
}

The custom attribute convention encapsulates the skinning and theming work in a structured way. It would be simpler and require less steps to override one string resource definition although this practice can become unwieldy in the event of many variants. This convention forces new variants to define their own Theme and the app will throw a RuntimeException for missed attributes. This structure can go a long way to ensure no brands get mixed and forces variants to consider each of the configurable item.

BuildConfig

There’d be nothing left to mention if alternate application versions consisted only of simple asset swaps. In most cases however unique products request different requirements, which requires handling alternates in Java code. It might not be practical to provide a different Java source class definitions in each of the flavor sources, especially when the core functionality lives in main sources and you need only a minor behavior change.

BuildConfig to the rescue. The build system compiles the product flavor for the given variant into BuildConfig. Java sources that live in src/main can conditionally check the BuildConfig.FLAVOR symbol and adjust behavior accordingly.

if (BuildConfig.FLAVOR.equals("dogs")) {
     showDailyWalkingReminder();
}

The BuildConfig.FLAVOR symbol is a great asset for a simple use-case but in a more complicated scenario may not be sufficient. Conditional checks like the above can quickly turn your codebase into a difficult-to-maintain-nest- of-if-then-statements. Writing code in this manner does not scale. Suppose next year PetMaster++ decides to roll out another brand for bird enthusiasts. Some of your conditional checks may no longer work as as intended for all of your brands and will require refactoring. Future behavior changes for a single variant would require changing this complicated if/then structure, necessitating extensive regression testing on all variants.

A better way

A better solution is to abstract business logic for features into an interface and require each product flavor to provide its own implementation of the the feature configuration interface. No more mental parsing of if-blocks; instead the implementations are separate, easy to find, and simpler to modify without being as concerned if other flavors are impacted. Code written in this manner is easier to conceptualize and is more future-proof as variants come and go.

Define a feature configuration in src/main:

public interface IFeatureConfig {
     boolean requiresDailyWalking();
}

Provide an implementation in each of the flavor source folders:

// src/pets
public class FeatureConfig implements IFeatureConfig {
    @Override boolean requiresDailyWalking() { return false; }
}

// src/cats
public class FeatureConfig implements IFeatureConfig {
    @Override boolean requiresDailyWalking() { return false; }
}

// src/dogs
public class FeatureConfig implements IFeatureConfig {
    @Override boolean requiresDailyWalking() { return true; }
}

// src/main client code
if (featureConfigInstance.requiresDailyWalking()) {
     showDailyWalkingReminder();
}

Thinking about this code even more, one could consider extracting out the functionality of showDailyWalkingReminder into src/dog.

Build variant uniqueness

The build system by default handles many of the steps required to assemble several unique applications from one codebase. The reality is the Android Gradle build system was written years after the framework was written and some components do not have built-in with the modern build system. One common example is providing a unique ContentProvider authority for each of your branded applications. This framework component requires a system-level unique identifier

In your application build.gradle you can iterate over the applicationVariants and generate a unique identifier string resource:

android.applicationVariants.all { variant ->
    variant.resValue "string", "authority_pet_food", "${variant.applicationId}.pet.food"
}

Refer to this string resource definition in your AndroidManifest.xml:

<provider     android:name="com.example.petmaster.PetFoodContentProvider"     android:authorities="@string/authority_pet_food"     android:exported="false"/>

This string resource is also available in Java sources using the R.string.authority_pet_food symbol. This workaround is relatively painless and allows for automatic generation of unique components. The resource is automatically generated during each build and more than one build variant can be successfully installed to a device without failing due to INSTALL_FAILED_CONFLICTING_PROVIDER error.

Sample code

Check out the PetMasterPlusPlus.tar.gz sample project included to see some sample build.gradle configuration and product flavor source set management. Our codebase has transformed to follow the approaches outlined in the post and rolling out new applications is easier than ever before. We’ve been forced learned a lot along the way and hope our experience helps you with the architecture of your application. We suspect our techniques will transform even more in the future. In particular, some improvement ideas include:

  • Flavors on demand. This would mean less build variants to compute for the Gradle model and increase build times
  • Eliminating the custom attr or improving the implementation. This abstraction may be unnecessarily complicated. A better implementation of styles or a codebase built with variant configuration in mind might not need this heavy-handed solution.

Thanks for reading and please comment with any questions or your own experiences with build variants and the Android Gradle build system.