Building multiple regions apps with product flavors ( Good and Bad )

In my company, we develop a Customer-to-Customer market place app that is available in the US and Japan. We launched the app in each country as different versions(i.e. it has the different package name in the US and Japan) so that we can create region specific functions for payments, delivery options and such. 
For Android, there’s a great way to split the apks with one single repository: Product Flavors. After years of developing two versions of apps with a single source code, I would like to share advantage and disadvantage (and some tips) about using product flavors to develop apps for multiple regions.

Setup product flavor variants

Before I go on to the discussion, let me share how we setup product flavors.
This is the code snippet of build.gradle. This creates 2 apps: “us” as in the US version and “jp” as in Japan version.

android {
productFlavors {
us {
minSdkVersion 15
applicationId PACKAGE_NAME_US
resConfigs “en”
buildConfigField “boolean”, “IS_ADROID_PAY”, “true”

}
jp {
minSdkVersion 14
applicationId PACKAGE_NAME_JP
resConfigs “ja”
buildConfigField “boolean”, “IS_ADROID_PAY”, “false”

}
}
}

Our app controls functionality by buildConfigField in each flavor variant. This will generate BuildConfig.java. resConfigs defines which resource files to load.

The directory structure looks like this:

src
├── main
│ ├── AndroidManifest.xml
├── java/com/…/
└── activity/

└── res/layout/…/
src
├── us
│ ├── AndroidManifest.xml
├── java/com/…/
└── activity/FuncActivity.java

└── res/layout/…/

├── jp
│ ├── AndroidManifest.xml
├── java/com/…/
└── activity/FuncActivity.java

└── res/layout/…/

Here, main directory has the source code in common with each flavor.
On the other hand, each flavor directories have codes with different implementation but have the identical file name.

Advantages

There are a lot of benefit you can get from product flavors.

Easy to manage and develop each flavor variant

This is the best (and only) reason why we use product flavors. Let’s see some use case.

Case1. Change visibility and functionality in each regions

Let’s say you have a screen which you want to control the visibility and functionality of a feature. Without product flavors, Activity would be like this:

onCreate(){

if(isUS) {
viewA.setVisiblilty(View.Visible);
viewA.setOnClickListener(this);
viewB.setVisiblilty(View.Gone);
} else if(isJP) {
viewA.setVisiblilty(View.Gone);
viewB.setVisiblilty(View.Visible);
viewB.setOnClickListener(this);
}
}

It’s going to be a total mess if these if clauses come up everywhere.
You can either create Activities or ViewHandler Class in each Flavors. 
I will write a snippet code for ViewHandler below.
In main directory:

public interface ViewHandler {
View createView(Context context);
}

In us directory write what you want to show:

public class ViewHandlerImpl {
View createView(Context context) {
View root = LayoutInflater.from(context).inflate(R.layout.view_search_sort, this);
View viewA = root.findViewById(R.id.view_a);
View viewB = root.findViewById(R.id.view_b);
viewA.setVisiblilty(View.Visible);
viewA.setOnClickListener(this);
viewB.setVisiblilty(View.Gone);
return root;
}
}

And in Activity(which is in main directory):

onCreate(){

ViewHandler viewHandler = new ViewHandlerImpl();
View controlView = viewHandler.createView(this);

}

Case2. Starting different Activities

There’s a case where in the US region I want to start FuncUSActivity.java, but in the Japan region I want to start FuncJPActivity.java. Using if clause, it would be like this:

public void startXXXProcess(){
if(isUs){
startActivity(new Intent(this, FuncUSActivity.class));
} else if(isJp) {
startActivity(new Intent(this, FuncJPActivity.class));
}
}

Introducing Navigator class helps handle this kind of situation.
In the US variant, create Navigator class:

public class Navigator {
public static startXXXProcess(Context context){
startActivity(new Intent(context, FuncUSActivity.class));
}
}

Where as in the JP variant:

public class Navigator {
public static startXXXProcess(Context context){
startActivity(new Intent(context, FuncJPActivity.class));
}
}

Now starting Activity is much simple with Navigator:

public void startXXXProcess(){
Navigator.startXXXProcess(Context context);
}

Case3. Library dependency

If you want to introduce a library only in a specific region, you can setup dependencies in build.gradle like this:

dependencies {
compile “com.android.support:support-v4:${support_library_version}”
usCompile “com.abc.library:moduleX:1.0.0”

Also, I would prefer using a wrapper class which encapsulate the functionality.

In the case above(moduleX is only in the US variant), the wrapper class would be like this:

public class WrapperModuleX {
private ModuleX moduleX;
public WrapperModuleX(){
moduleX = new ModuleX();
}

public void doAlpha() {
moduleX.doAlpha();
}

}

And for the JP variant, create the same interfaces but nothing written inside:

public class WrapperModuleX {
public WrapperModuleX(){
}
public void doAlpha() { }

}

When you call ModuleX:

WrapperModuleX moduleX;
onCreate(){
moduleX = new WrapperModuleX();
moduleX.doAlpha()
}

DisAdvantages

I have to admit that there are some hassle to go with product flavors.
All of them relate to scalability issue.

Too many directories to take care

For instance, If you are using product flavors, you have to split the test directory as well in order to run tests for each variant. In our case, we have the US and Japan flavor so there are test, testUs, testJp directories. This is same for debug and release buildTypes, too. I think 3 is the maximum variants developers can handle.

Running CI takes longer time

This can be a serious bottle neck for developments. There’s not much we can do to prevent this. We are currently using Travis CI for release build check and Circle CI for debug build check and testing to reduce the running time.

Code duplication

In some cases, only one or two methods of a class have a different functionality in each variant. You have to take care of both variants if you need to fix functions that are in common.
To avoid duplication of the code, Creating abstract class in main directory might help.
For instance, let’s say there is an address validation class which zipcode validation is different in the US and Japan.

In the main directory, create an abstract class like this:

abstract class AddressValidation {
public Result validateCountoryCode(String country){

}
public Result validateCityName(String city) {

}
abstract public Result validateZipcode(String zipcode);
}

For each variant, create subclasses and provide implementations to the abstract method.

public class AddressValidationImpl {
public Result validateZipcode(String zipcode){

}
}

Conclusion

Product flavors is a great way to split functionality and control multiple apks. However, it can easily reach a breaking point when you try to scale your project. 
So my suggestions for using product flavors for multiple regions are as follows:

  • If you only have a plan to develop an app for 2 or 3 countries, product flavors are good way to manage your code and keep the source code readability high.
  • If you are considering extending your service to more than 3 countries but as different apks, it’s better to split your project in each region. For common functionalities, modularize them and add into your project as submodules.