Cover photo by James Sutton, unsplash In this series, I have described why I make the 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.

Don’t forget to call your dad too 👴

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:

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:

<!-- 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:

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

Cover photo by James Sutton on Unsplash

Posted by Anders Tornblad on Category Apps Labels
Tweet this