Don't forget to call your dad too
This is part 4 of the Making "Call mom" series. If you haven't read the first part, here it is: Don't forget to call your mom
In this series, I have described why I make the Android apps named Call Mom and Call Dad, and some of the issues with creating a phone call reminder app, like using persistant storage, showing notifications and setting alarms. This fourth and final part describes how you can build multiple apps with similar functionality without creating unnecessary code duplication. If you just want the summary and some links, go to the Summary section at the bottom.
As I explained in the first article of this series, my reason for creating the Call Mom app was that my mother passed away. At first, I used the app for calling my dad, so I soon started looking into how to create two very similar apps with different names, and some small differences in text and color resources.
My first attempt involved a huge refactoring. All the parts common to both apps were broken out into its own module, without any resources, and every Activity
class was made into an abstract
class. Then, in each app module, a non-abstract implementation of each activity class was created, connecting it to the resources found in that app module. This method quickly became completely unsustainable, and I started looking for a better way.
Flavor dimensions to the rescue
After a while, I learned about product flavors. This mechanism is mainly used for creating different app variants. If you want to publish a free app, and also provide a paid version with some more functionality, product flavors is probably what you are looking for. I had already started thinking about what I could add to my apps to make "premium" versions to sell. This might not happen for Call Mom and Call Dad, but I did understand that the two apps could be realized as two different flavors of the same app.
This made the development much easier. Instead of three messy modules, I only needed one app module. The first step was to configure a flavor dimension named callwho
, and then one product flavor for each app, where the last part of each app's package name is set:
// build.gradle file for the app module - not the project
apply plugin: 'com.android.application'
android {
defaultConfig {
applicationId "se.atornblad"
...
}
flavorDimensions "callwho"
productFlavors {
calldad {
dimension "callwho"
applicationIdSuffix ".calldad"
}
callmom {
dimension "callwho"
applicationIdSuffix ".callmom"
}
}
...
}
The configuration shown above will create two different "callwho" flavors of my application:
- Product flavor "calldad", application id
se.atornblad.calldad
- Product flavor "callmom", application id
se.atornblad.callmom
With just this one configuration change, you can now compile and package two apps that are identical. Instant code sharing with zero code duplication. What makes this interesting is when you introduce the differences between the two apps flavors.
Start by creating flavor-specific resources for each app. If you have just recently created your app, you probably have one single strings.xml
file that looks something like this:
<!-- This is ./src/main/res/values/strings.xml -->
<resources>
<string name="app_name">Call Reminder App</string>
<string name="action_settings">Settings</string>
<string name="call_button_text">Call Now</string>
...
</resources>
A mistake that would be easy to make is to copy this file twice and change the values that are different between app flavors. That would introduce a lot of duplication. Instead, create one override xml file for each app. Start by creating two new strings.xml
files:
./src/calldad/res/values/strings.xml
./src/callmom/res/values/strings.xml
<!-- This is ./src/calldad/res/values/strings.xml -->
<resources>
<string name="app_name">Call Dad</string>
<string name="call_button_text">Call Dad Now</string>
...
</resources>
<!-- This is ./src/callmom/res/values/strings.xml -->
<resources>
<string name="app_name">Call Mom</string>
<string name="call_button_text">Call Mom Now</string>
...
</resources>
After saving these files, when you look at the Project tool window, you will only see one of them. It will look something like this:
> values
colors.xml
dimens.xml
> strings (2)
strings.xml
strings.xml (calldad)
styles.xml
This is when I started thinking I had done something wrong. I double-checked and double-double-checked my build.gradle
file, and meticulously checked the spelling of every directory and strings.xml
file that I had. Don't worry! This is just Android Studio telling you which product flavor is the active one.
To switch between product flavors and build types, open the Build Variants tool window. That's where you can switch between your app's different variants. Switch from calldadDebug
to callmomDebug
, and your resources will look like this:
> values
colors.xml
dimens.xml
> strings (2)
strings.xml
strings.xml (callmom)
styles.xml
Go through the same process to create flavor-specific layouts, drawables, menus and other resource types.
If you want flavor-specific variants of your Java or Kotlin classes, create a folder called ./src/calldad/java
and mimic the directory structure of ./src/main/java
. If the flavor-specific java
directory contains a file named exactly like in the non-specific main
directory, the specific one will be used instead.
Multidimensional app configurations 🧊
Android Studio is not limited to just one flavor dimension. You could, for example, create one callwho
flavor to separate calling mom from calling dad, and another market
flavor to separate a free ad-supported app from a paid premium one with extra features.
Add another dimension to your app module's build.gradle
file:
// build.gradle file for the app module - not the project
apply plugin: 'com.android.application'
android {
defaultConfig {
applicationId "se.atornblad"
buildConfigField 'boolean', 'ENABLE_ADMOB', 'false'
...
}
flavorDimensions "callwho", "market"
productFlavors {
calldad {
dimension "callwho"
applicationIdSuffix ".calldad"
}
callmom {
dimension "callwho"
applicationIdSuffix ".callmom"
}
free {
dimension "market"
buildConfigField 'boolean', 'ENABLE_ADMOB', 'true'
}
premium {
dimension "market"
applicationIdSuffix ".premium"
}
}
...
}
This gradle configuration would produce four different apps:
- Call Mom (free), application id
se.atornblad.callmom
- Call Dad (free), application id
se.atornblad.calldad
- Call Mom (premium), application id
se.atornblad.callmom.premium
- Call Dad (premium), application id
se.atornblad.calldad.premium
One detail that you might notice is that I have created a build configuration field called ENABLE_ADMOB
, which is set to false
by default, but is set to true
for the free
flavor. To check this field in an Activity
method, you can do something like this:
@Override
protected void onCreate(Bundle savedInstanceState) {
if (BuildConfig.ENABLE_ADMOB) {
MobileAds.initialize(this);
// ... continue to initialize AdMob ads
}
}
Summary
- Use product flavors to generate multiple variants of your app
- Override resources or code that are specific to each variant
- Put specific resources in the
./src/FLAVOR/res
directory - Put specific code in the
./src/FLAVOR/java
directory - Use as many flavor dimensions as you need
Articles in this series: