Author Archives: Vishnumoorthi Bhat

Vishnumoorthi Bhat
Vishnu is a Technical Architect at Robosoft. He has an in-depth knowledge and extensive work experience in iOS, Android and ReactNative platforms. His expertise lies in managing and developing Hybrid Mobile Applications for both Android /iOS Platforms, Technical Design, Design patterns and UML.
Mobile Technologies Opinion

Configuring iOS Builds – a step-by-step guide

Build configuration is a way of arranging, tweaking and changing the settings of a build using a build system. The configuration defines how a build generation process should work, what properties should be applied to the build and what all aspects should be taken care of. In our earlier article, we outlined how to configure Android builds. In this article, we will talk about why developers must configure iOS builds and how to do it. Read on.

Why configure?

  • There are various reasons to configure builds. A few of them are;
  • If a project requires to support multiple environments like DEV, QA, STAGING and PROD
  • If a project requires different build types (like debug and release)
  • If a project requires a way to modify dependencies without changing the application source code
  • If project requires to have builds automated
  • If a project has different API keys for different environments. This might be required for each library integrated within the application
  • If a project has a requirement not to commit keys and secrets it uses to source code management system
  • If a customer wants builds of different environments to co-exist in the device

These are only a handful. There might be other reasons too.

It’s recommended to store our keys and secrets securely. In a real application, we would either get these from our server or encrypt and store them using standard encryption techniques.

The fields shown here are for demonstration purposes only.

It’s also important not to commit secure configuration properties to SCM (Git/SVN), especially, if SCM is in a public repository.

Utilise techniques such as .gitignore to prevent these from being committed to SCM.

We can also have template config files with only keys (with values being empty) while committing. We can get these inserted to build systems before generating the builds.

How to Configure?

Before jumping onto see how to configure builds in iOS, let’s assume our requirements so that it will be easy for us to scope the discussion. Our assumptions are;

  • Our project has three different ENVs (environments) namely, dev (development), stg (staging) and prod (production)
  • We want to use different API URLs, keys and secrets for each ENVs
  • We want to use different bundle identifiers for each of the ENVs so that, builds with different ENVs can co-exist in a device
  • We want to use labelled app icons for dev and staging

Now that we know what all we have to do, let’s dive in to see how to do them.

Xcode provides fantastic support for configuring builds in iOS. It has a build settings pane where we can add/edit build settings, it has a scheme editor pane where we can create multiple schemes and it also provides special type of files with extension .xcconfig to support configuration supply to the build system.

Our application is called Places. It allows users to add and share details of the places they visit. Let’s start by adding three configuration settings files to our project.

Configuration settings files are provided in Xcode for specifying build settings. These files have an extension .xcconfig and are used to define and override build settings for a build configuration of a project or target.

More on these files can be found at https://help.apple.com/xcode/mac/current/#/dev745c5c974

In Xcode, goto menu File → New → File… (or press ⌘N). Locate Configuration Settings File and click Next.

Locate Configuration Settings File and click Next.

Name it dev.xcconfig. Leave Targets checkbox as is. Click on Create.

Name it dev.xcconfig. Leave Targets checkbox as is. Click on Create.

Repeat the above steps to add stg.xcconfig and prod.xcconfig files. These config files we just added are plain text files which can be edited even outside of Xcode. They are ideal for supplying build settings and other configurable properties to the build system without modifying the source code. Hence, if we have a requirement not to commit these config files to SCM, we can omit them.

Contents of these files are key-value pairs and they take the form Key = Value. Various value types are supported. A few of them are – boolean, string, string list, path and path list.

Now that we have added three files, they should look like as shown in below image.

Now that we have added three files, they should look like as shown in below image.

Our app development connects to our server to fetch places data which has three different ENVs namely dev, stg and prod. Hence, the base URLs are;

Dev: http://api.places.com/dev/ (Note the http scheme)
Stg: https://api.places.com/stg/
Prod: https://api.places.com/

Also, we would name our app bundle identifiers as;

Dev: com.places.ios.dev
Stg: com.places.ios.stg
Prod: com.places.ios

As you can see above, the base URLs and bundle identifiers have some components in common. URLs have same host components. Bundle identifiers have com.places.ios as common. Hence, instead of repeating these in all three files, we can have a common.xcconfig file and define them there. Later, we can include this file in all three files.

Add common.xcconfig by following previous steps. Now add the common properties in common.xcconfig so that it looks like below.

Add common.xcconfig by following previous steps.

Since our schemes differ for dev and other two configurations, we use a separate SCHEME field in the common.xcconfig and set it to https (imagine it as a default value).

Notice that we have also added BUNDLE_VERSION_STRING and BUILD_NUMBER fields to the common.xcconfig. Moving forward, we can change application version and build number directly from config file.

Now, let’s include these in other three files and add ENV specific properties. Our dev.xcconfig now looks like below.

#include "common.xcconfig"
 
SCHEME = http
 
// ENV specific
ENV_NAME = dev
 
// Bundle Id suffix
BUNDLE_ID_SUFFIX = .$(ENV_NAME)
 
// API path
API_PATH = $(ENV_NAME)/
 
// Analytics API key and secret
ANALYTICS_API_KEY = 783fa804f48d2952c22bf6653ce4474f
ANALYTICS_API_SECRET = f93754758b5b7f242f89b8d38223e836

#include statement in the first line imports build configurations from common.xcconfig file. Property ENV_NAME is assigned a value of dev so that, this can be substituted in other required places. Notice how we are substituting value of ENV_NAME property to BUNDLE_ID_SUFFIX and API_PATH properties using $() syntax. Later, we are going to construct complete base URL and bundle identifiers using these respectively. Last two lines represent API key and secret required by the analytics SDK we are planning to integrate. As we want to keep analytics specific to ENVs, we use different keys and secrets for dev, stg and prod. Similarly, if we have any other ENV specific properties, we can add them to respective .xcconfig files.

Below are the contents of stg.xcconfig file

#include "common.xcconfig"
 
// ENV specific
ENV_NAME = stg
 
// Bundle Id suffix
BUNDLE_ID_SUFFIX = .$(ENV_NAME)
 
// API path
API_PATH = $(ENV_NAME)/
 
// Analytics API key and secret
ANALYTICS_API_KEY = 8b383b32443c343d8e97ae2a2cbbb986
ANALYTICS_API_SECRET = e335d54b5043dcda6ee13a688a7539fc

and prod.xcconfig file.

#include "common.xcconfig"
 
// ENV specific
ENV_NAME =
 
// Bundle Id suffix
BUNDLE_ID_SUFFIX = 
 
// API path
API_PATH = 
 
// Analytics API key and secret
ANALYTICS_API_KEY = b76748fc63ede06d366f0e55cb447ded
ANALYTICS_API_SECRET = 78e6376ebbab8cf71c5cbc388e7f24cb

Notice in prod.xcconfig, we have omitted BUNDLE_ID_SUFFIX and API_PATH values, as they are not required (Remember, our prod URL is api.places.com/ and prod bundle id is com.places.ios, which are already mentioned in common.xcconfig). Also note that, we haven’t added SCHEME field in neither stg nor prod configs. They take the default value of https mentioned in common.xcconfig.

In case you have same schemes (say https) for all ENVs, we can construct the URL with scheme and host together in a single BASE_URL field like https://api.places.com/

However, there is a catch. Settings configuration files do not directly support URLs. If you try adding a full URL with scheme and host as above, substring that starts with // is treated as comment and when it’s read we will only see https:
Even wrapping them in double quotes does not work.

The solution is to use a dirty trick to reconstruct the URL as below.

BASE_URL = https:/$()/api.places.com/

The $() above substitutes the empty string in between forward slashes.

Now that we have added and filled our config files, let’s inform Xcode where to use them. Go to project navigator, click on your project file and in the editor area, select Places under PROJECT section as shown below.

Configurations

In the above image, under Info tab, notice the Configurations section marked as #3. It already contains Debug and Release as two default configurations (#4). Since we have three different ENVs, we would like to have Debug and Release configurations for each of the ENVs. Click on the + button below the Configuration section and select Duplicate “Debug” Configuration option. A new configuration named Debug copy will be created in an editable format .Change the name of the configuration to Debug(dev). Similarly, create one more copy of Debug and rename it to Debug(stg).

Repeat the same process to create two copies of Release configuration (by selecting Duplicate “Release” Configuration option) and name them Release(dev) and Release(stg). Now we have created Debug and Release configurations corresponding to dev and stg ENVs. Let’s keep the original Debug and Release configurations for prod. Once completed, they look like as shown in the image below.

 “Release” Configuration

Now, let’s link our .xcconfig files to these configurations we just created. Click on the small disclosure triangle placed left to Debug configuration. When it is disclosed, it shows a project file and a target below it. Click on the popup button in front of the project file and select prod from the options. Similarly, disclose Debug(dev) and choose dev from the popup in front. Repeat this for all the configurations so that the section looks like as shown in below image.

Config_set

In the above image, under each configuration, we have a project file icon and a target icon. It’s possible to have multiple targets under the one project. We are adding our settings configuration files at project level. This indicates, the target under that project also gets those settings.

By default, project level settings are carried over to targets belonging to the project. However, each target can override these settings if needed.

In case we are planning to use CocoaPods for our dependencies, then target specific .xcconfig files are added by CocoaPods automatically.

Let’s add Crashlytics to our project using CocoaPods. Close the Xcode project if it’s open already. Open a terminal window and cd to our projects base directory. Create a Podfile using command below (assuming our project directory is in user’s Desktop directory).

Vishnus-MacBook-Pro:~ vishnu$ cd ~/Desktop/Places
Vishnus-MacBook-Pro:Places vishnu$ vi Podfile

In the window that opens vi editor, enter character i so that vi goes into edit mode. Enter below code.

platform :ios, ‘10.0’

target ‘Places’ do
use_frameworks!

pod ‘Fabric’
pod ‘Crashlytics’
end

Press ESC key then : (colon). At the colon prompt that appears, enter wq keys to save and quit vi. Now do a pod install. Terminal looks as below.

Vishnus-MacBook-Pro:~ vishnu$ pod install
Analyzing dependencies
Downloading dependencies
Using Crashlytics (3.10.2)
Using Fabric (1.7.7)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.
Vishnus-MacBook-Pro:Places vishnu$

Now go to the project base folder in the Finder and open Places.xcworkspace file by double clicking it. From now on, we’ll use Places.xcworkspace to open our project instead of Places.xcodeproj.

If you are configuring an existing project which already has CocoaPods integrated, just delete <ProjectName>.xcworkspace, Podfile.lock and Pods folder and redo a pod install. This will recreate pod specific .xcconfig files.

Now if we open our .xcworkspace file and check configurations section, pod specific .xcconfigs will be added for each target, as shown in below image.

.xcworkspace file and check configurations section

Now that we have our configurations ready, let;’s check where these are available for us as settings. Select project file in navigation area → select Places under TARGETS section → select Build Settings tab → scroll down to User-Defined section, as shown in the image below. All fields added in .xcconfig files will be available here.

select Places under TARGETS section → select Build Settings tab → scroll down to User-Defined section

We have added these as build settings. So, they will be available for the build system while building. But, we need to access some of these in code (ex: BASE_URL). For this, we’ll have to expose these to our Info.plist file, so that those fields can be available to us at runtime. Let’s add those fields to our Info.plist file by editing it (We can add new entries by going to Editor menu → Add Item or by Control click → Add Row).

After this, our Info.plist looks like as shown in the image below.

Editor menu → Add Item or by Control click → Add Row)

We have added BaseURL, APIKey and APISecret entries and substituted values from build configurations. Notice how values are constructed from components to make final values.

BaseURL has a value $(SCHEME)://${BASE_URL}$(API_PATH)
If we take dev.xcconfig values and replace above keys, we get http://api.places.com/dev/

Similarly, we have edited few existing keys to give new values from our .xcconfig files. We have modified Bundle identifier, Bundle name, Bundle version string, short and Bundle version so that we can control these from our configs.

Now that we have our Info.plist ready, let’s see how we can make it update based on the ENVs that we have. Also, let’s examine how to generate builds based on ENVs. Notice that, above we have modified our Info.plist to name our application based on ENV, that is, $(PRODUCT_NAME)$(BUNDLE_ID_SUFFIX) (Ex: Places.dev, Places.stg).

Select scheme menu at the top left corner of the Xcode toolbar and choose Manage Schemes… as shown in the image below.

Select_manage_schemes

In the window that opens, select Places row and click on gear icon below. Select Duplicate as shown below.

Select Duplicate

In the new panel that opens with editable name field, rename scheme to Places(Dev) and click Close button as shown below.

Rename scheme to Places(Dev) and click Close button

Repeat the above steps to create Places(Stg) scheme. When done, the list of schemes will be as shown in below image.

Scheme_list

Notice the Shared column with checkboxes in the image above. Make sure they are checked for all the schemes we added. If these are unchecked, the schemes will not be available to others, when you check-in your code.

Now, we need to define the build types for each of the tasks. Double-lick on the Places(Dev) row above to go to the edit scheme screen. For each of the tasks listed in left, change the build configuration to respective configuration, as shown in the example image below.

Edit_configs

Run → Info tab → Build Configuration → Select Debug(dev)
Test → Info tab → Build Configuration → Select Debug(dev)
Profile → Info tab → Build Configuration → Select Release(dev)
Analyze → Build Configuration → Select Debug(dev)
Archive → Build Configuration → Select Release(dev)
Repeat the above steps for Places(Stg) scheme as well.

Once the scheme editing is done, let’s see if things we have done till now are in place. Let’s try to access keys we have added in our Info.plist so that we can verify if we are getting expected values at runtime. Open ViewController.swift file and change viewDidLoad method to look like below.

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
   
    print("Name: (Bundle.main.infoDictionary?["CFBundleName"]! ?? "")")
    print("Version: (Bundle.main.infoDictionary?["CFBundleShortVersionString"]! ?? "")")
    print("Build#: (Bundle.main.infoDictionary?["CFBundleVersion"]! ?? "")")
    print("Base URL: (Bundle.main.infoDictionary?["BaseURL"]! ?? "")")
    print("API Key: (Bundle.main.infoDictionary?["APIKey"]! ?? "")")
    print("API Secret: (Bundle.main.infoDictionary?["APISecret"]! ?? "")")
  }

Select Places(Dev) from the scheme menu and run the application. You will see logs like below in the console.

Name: Places.dev
Version: 0.1
Build#: 1
Base URL: http://api.places.com/dev/
API Key: 783fa804f48d2952c22bf6653ce4474f
API Secret: f93754758b5b7f242f89b8d38223e836

These output values correspond to the values we have configured for the dev.xcconfig file. Try selecting other schemes from scheme menu and observe the output. We can also observe that, once these schemes are run, three separate apps co-existing in the device/simulator.

Apps_homescreen

Those icons are default and not looking great. Let’s see how we can add scheme specific icons now.

Open Assets.xcassets folder in Xcode sidebar and click on AppIcon and drag & drop all the icons for prod scheme as shown below.

Prod_icons

To add dev and stg icons, create a new iOS App Icon set, by clicking + button below, select App Icons & launch Images → New iOS App Icon. Rename it to AppIcon-dev as shown in below image. Add all icons. Repeat the above steps to create AppIcon-stg app icon set as well.

App_iconset

Finally, let Xcode know which app icon set to use for each build. Open target settings → select Build Settings tab, search for AppIcon. In the Asset Catalog App Icon Set Name settings, edit the app icon set names for respective configurations as shown below.

Set_icons

Build and run all the schemes to see three separate apps installed on the device/simulator with respective icons (Preview app in OS X can be used to add labels on the icons).

(Preview app in OS X can be used to add labels on the icons).

One last thing related to configuration. Sometimes, we might need to include bespoke files based on environments in the build. One such example is GoogleService-Info.plist file which we download from google console and include in the project, for google API services. In case we need to maintain separate GoogleService-Info.plist file, one per environment, then we can achieve this by creating a custom run script build phase in Xcode.

Start by creating three folders inside your project target directory namely dev, stg and prod. Copy respective GoogleService-Info.plist files downloaded from Firebase to respective directories created.

Google_services_plist

In Xcode, go to project target, select Build Phases tab and click on the + button, select New Run Script Phase as shown below. Rename the run script phase to Copy GoogleService-Info.plist.

New_run_script

Expand the disclosure triangle to edit the run script phase. In the script editor area, add the script to copy respective GoogleService-Info.plist file to the app bundle as shown below.

Copy_script

Below is the script to copy file.

# Name of the file we're copying
GOOGLESERVICE_INFO_PLIST="GoogleService-Info.plist"
 
# Reference to the destination location for the GoogleService-Info.plist
PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
 
# Target directory of our project
TARGET_ROOT=${PROJECT_DIR}/${TARGET_NAME}
 
# Check and copy files
if [ "${ENV_NAME}" == "dev" ]
then
 
    SOURCE_FILE_PATH="${TARGET_ROOT}/dev/${GOOGLESERVICE_INFO_PLIST}"
    echo "Source: $SOURCE_FILE_PATH"
 
    if [ ! -f $SOURCE_FILE_PATH ]
    then
        echo "No Development GoogleService-Info.plist found. Aborting!"
        exit 1
    else
        echo "Copying GoogleService-Info.plist from dev directory"
        cp "${SOURCE_FILE_PATH}" "${PLIST_DESTINATION}"
    fi
 
elif [ "${ENV_NAME}" == "stg" ]
then
 
    SOURCE_FILE_PATH="${TARGET_ROOT}/stg/${GOOGLESERVICE_INFO_PLIST}"
    echo "Source: $SOURCE_FILE_PATH"
 
    if [ ! -f $SOURCE_FILE_PATH ]
    then
        echo "No Staging GoogleService-Info.plist found. Aborting!"
        exit 1
    else
        echo "Copying GoogleService-Info.plist from stg directory"
        cp "${SOURCE_FILE_PATH}" "${PLIST_DESTINATION}"
    fi
 
else
    SOURCE_FILE_PATH="${TARGET_ROOT}/prod/${GOOGLESERVICE_INFO_PLIST}"
    echo "Source: $SOURCE_FILE_PATH"
 
    if [ ! -f $SOURCE_FILE_PATH ]
    then
        echo "No Production GoogleService-Info.plist found. Aborting!"
        exit 1
    else
        echo "Copying GoogleService-Info.plist from prod directory"
        cp "${SOURCE_FILE_PATH}" "${PLIST_DESTINATION}"
    fi
fi

This article was in continuation of – Configuring Android Builds – a Step-by-Step Guide.

Read More
Mobile Technologies Opinion

Configuring Android Builds – a step-by-step guide

Build configuration is a way of arranging, tweaking and changing the settings of a build using a build system. The configuration defines how a build generation process should work, what properties should be applied to the build and what all aspects should be taken care of.

In this article, we will outline why developers must configure builds and how to do it. Read on.

Why configure?

There are various reasons to configure builds. A few of them are;

  • If a project requires to support multiple environments like DEV, QA, STAGING and PROD
  • If a project requires different build types (like debug and release)
  • If a project requires a way to modify dependencies without changing the application source code
  • If project requires to have builds automated
  • If a project has different API keys for different environments. This might be required for each library integrated within the application
  • If a project has a requirement not to commit keys and secrets it uses to source code management system
  • If a customer wants builds of different environments to co-exist in the device
  • These are only a handful. There might be other reasons too.

It’s recommended to store our keys and secrets securely. In a real application, we would either get these from our server or encrypt and store them using standard encryption techniques.

The fields shown here are for demonstration purpose only.

It’s also important not to commit secure configuration properties to SCM (Git/SVN), especially, if SCM is in a public repository.

Utilise techniques such as .gitignore to prevent these from being committed to SCM.

We can also have template config files with only keys (with values being empty) while committing. We can get these inserted to build systems before generating the builds.

How to Configure?

Before jumping onto see how to configure builds in Android, let’s assume our requirements so that it will be easy for us to scope the discussion. Our assumptions are;

  • Our project to have three different ENVs (environments) namely, dev (development), stg (staging) and prod (production)
  • We want to use different API URLs, keys and secrets for each ENVs
  • We want to use different application IDs for each of the ENVs so that, builds with different ENVs can co-exist in a device
  • We want to use labelled app icons for dev and staging

Now that we know what all we have to do, let’s dive in to see how to do them.

Android Studio uses Gradle as the default build system. Gradle allows us to configure builds using build.gradle file entries, where we can define multiple build types, add product flavours and come up with build variants.

For the purpose of this article, let’s assume an application called Places. It allows users to add and share details of the places they visit. Let’s see how we can setup three different environments of our app.

Gradle scripts (build.gradle files) in Android Studio are written in a Domain Specific Language (DSL) called Groovy. It allows us to read external files from within the script so that we can separate out our configurable properties.

More on groovy can be found at http://groovy-lang.org/

We can achieve external configurations read in various ways in Android. We can store different environment properties in xml files, which are placed in respective flavour directories. Or we can create different .properties files and read the configurations from them during build. Or we can directly add them in build.gradle. However, as we want to secure our properties by keeping them in separate files, let’s go with creating .properties files.

This article walks through the configuration steps on a Mac OS. Same steps hold good for Windows and Linux systems too.

Create a new directory named config in the project root directory.

Create a new directory named config in the project root directory.

Create three files namely dev.properties, stg.properties and prod.properties in the config directory created as shown in the image below.

config directory

These config files we just added are plain text files which can be edited using any text editor. They are ideal for supplying build settings and other configurable properties to the Gradle build system without modifying the source code. Hence, if we have a requirement not to commit these config files to SCM, we can omit them.

Contents of these files are key-value pairs and they take the form Key = Value. Various value types are supported. A few of them are – boolean, string, number.

Our app connects to our server to fetch places data which has three different ENVs namely dev, stg and prod. Hence, the base URLs are;

Dev: http://api.places.com/dev/ (Note the http scheme)
Stg: https://api.places.com/stg/
Prod: https://api.places.com/

Also, we would name our app bundle identifiers as;

Dev: com.places.android.dev
Stg: com.places.android.stg
Prod: com.places.android

Notice that we have also added VERSION_CODE and VERSION_NAME fields to the .properties files. Moving forward, we can change application version number and version string directly from these .properties files.

Our dev.properties now looks like below.

# Bundle Id
APPLICATION_ID = "com.places.android.dev"
 
# Bundle versions string, short
VERSION_CODE = "1"
VERSION_NAME = "0.1"
 
# Base URL
BASE_URL = "http://api.places.com/dev/"
 
# Analytics API key and secret
ANALYTICS_API_KEY = "783fa804f48d2952c22bf6653ce4474f"
ANALYTICS_API_SECRET = "f93754758b5b7f242f89b8d38223e836"

Last two lines represent API key and secret required by the analytics SDK we are planning to integrate. As we want to keep analytics specific to ENVs, we use different keys and secrets for dev, stg and prod. Similarly, if we have any other ENV specific properties, we can add them to respective .properties files.

Below are the contents of stg.properties file

# Bundle Id
APPLICATION_ID = "com.places.android.stg"
 
# Bundle versions string, short
VERSION_CODE = "1"
VERSION_NAME = "0.1"
 
# Base URL
BASE_URL = "https://api.places.com/stg/"
 
# Analytics API key and secret
ANALYTICS_API_KEY = "8b383b32443c343d8e97ae2a2cbbb986"
ANALYTICS_API_SECRET = "e335d54b5043dcda6ee13a688a7539fc"

and prod.properties file.

# Bundle Id
APPLICATION_ID = "com.places.android"
 
# Bundle versions string, short
VERSION_CODE = "1"
VERSION_NAME = "0.1"
 
# Base URL
BASE_URL = "https://api.places.com/"
 
# Analytics API key and secret
ANALYTICS_API_KEY = "b76748fc63ede06d366f0e55cb447ded"
ANALYTICS_API_SECRET = "78e6376ebbab8cf71c5cbc388e7f24cb"

Note that, url schemes of BASE_URL for stg and prod are https.

Notice the key-value pairs added in the above files. They are all mentioned as strings within quotes. We are doing it purposely as we are reading them from within gradle files. More on this later.

Now that we have added and filled our config files, let’s inform Gradle where to use them. The idea is to read these configuration files based on the need and assign properties to build configuration so that, they can be read in our code. Some of the properties like APPLICATION_ID, VERSION_CODE and VERSION_NAME can also be substituted in our build.gradle file at build time.

Let’s open our project level build.gradle file in Android Studio and add an ext (ExtraPropertiesExtension) entry in it like below.

ext {
   // Get the current flavor of the build Ex: dev, stg, prod
   flavor = getCurrentFlavor()
   if (flavor.isEmpty()) {
       flavor = "dev"
   }
 
   // Read the .properties for config
   config = getProps('config/' + flavor + '.properties')
}

We are doing two things in above code block. First, identifying the current build flavor (ex: dev, stg or prod). Second, read the corresponding .properties file located in the config directory. That is, if the current build flavor is dev, read dev.properties file and save it in config variable.

We haven’t created build flavors yet. We’ll get to it moving on.

We also have two methods being called in the above code getCurrentFlavor() and getProps(). Let’s check what they look like.

def getCurrentFlavor() {
   Gradle gradle = getGradle()
 
   // match optional modules followed by the task
   // [a-z]+([A-Za-z]+) will capture the flavor part of the task name onward (e.g., assembleDevRelease --> Dev)
   def pattern = Pattern.compile("([A-Z][A-Za-z]+)(Release|Debug)")
   def flavor = ""
 
   gradle.getStartParameter().getTaskNames().any { name ->
       Matcher matcher = pattern.matcher(name)
       if (matcher.find()) {
           flavor = matcher.group(1).toLowerCase()
           return true
       }
   }
 
   return flavor
}

The above method extracts the flavor name from one of the gradle tasks and returns. It uses a regex to do this job.

def getProps(path) {
   Properties props = new Properties()
   props.load(new FileInputStream(file(path)))
   return props
}

getProps() reads a properties file from given path and returns its contents.

The complete project level build.gradle is given below. Notice the import statements for Matcher and Pattern classes utilised in our custom methods.

import java.util.regex.Matcher
import java.util.regex.Pattern
 
// Top-level build file where you can add configuration options common to all sub-projects/modules.
 
buildscript {
   ext.kotlin_version = '1.3.11'
   repositories {
       google()
       jcenter()
      
   }
   dependencies {
       classpath 'com.android.tools.build:gradle:3.3.0'
       classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
       // NOTE: Do not place your application dependencies here; they belong
       // in the individual module build.gradle files
   }
}
 
allprojects {
   repositories {
       google()
       jcenter()
      
   }
}
 
ext {
   // Get the current flavor of the build Ex: dev, stg, prod
   flavor = getCurrentFlavor()
   if (flavor.isEmpty()) {
       flavor = "dev"
   }
 
   // Read the .properties for config
   config = getProps('config/' + flavor + '.properties')
}
 
def getProps(path) {
   Properties props = new Properties()
   props.load(new FileInputStream(file(path)))
   return props
}
 
def getCurrentFlavor() {
   Gradle gradle = getGradle()
 
   // match optional modules followed by the task
   // [a-z]+([A-Za-z]+) will capture the flavor part of the task name onward (e.g., assembleDevRelease --> Dev)
   def pattern = Pattern.compile("([A-Z][A-Za-z]+)(Release|Debug)")
   def flavor = ""
 
   gradle.getStartParameter().getTaskNames().any { name ->
       Matcher matcher = pattern.matcher(name)
       if (matcher.find()) {
           flavor = matcher.group(1).toLowerCase()
           return true
       }
   }
 
   return flavor
}
 
task clean(type: Delete) {
   delete rootProject.buildDir
}

Let’s open the app (module) level build.gradle and see how we can substitute the properties read earlier.

Let’s open the app (module) level build.gradle

Under the android block, add flavorDimensions and productFlavors properties. Here we are specifying our build flavors to contain three flavors. Notice the flavor names and our .properties file names match. This helped us earlier in the project level build.gradle to read .properties files dynamically.

def envConfig
flavorDimensions "default"
productFlavors {
   dev {
       envConfig = getProps("../config/dev.properties")
       applicationId envConfig.getProperty("APPLICATION_ID").replace(""", "")
       versionCode envConfig.VERSION_CODE.replace(""", "").toInteger()
       versionName envConfig.VERSION_NAME.replace(""", "")
   }
   stg {
       envConfig = getProps("../config/stg.properties")
       applicationId envConfig.getProperty("APPLICATION_ID").replace(""", "")
       versionCode envConfig.VERSION_CODE.replace(""", "").toInteger()
       versionName envConfig.VERSION_NAME.replace(""", "")
   }
   prod {
       envConfig = getProps("../config/prod.properties")
       applicationId envConfig.getProperty("APPLICATION_ID").replace(""", "")
       versionCode envConfig.VERSION_CODE.replace(""", "").toInteger()
       versionName envConfig.VERSION_NAME.replace(""", "")
   }
}

We are also reading .properties file in respective flavor and assign the values to applicationId, versionCode, and versionName fields. Let’s examine the above lines a bit. We are substituting applicationId using envConfig.APPLICATION_ID.replace(“””, “”). Here we are using the app id specified in the respective config file. All our config property values are specified as string within double quotes. Gradle generates BuildConfig.java file on the fly while building and uses the values specified in the substitutions. Hence, we make sure to strip double quotes using replace(“””, “”).

versionCode is an integer and is substituted with value of VERSION_CODE by converting the string value to integer.

Change the properties under defaultConfig block as shown below.

defaultConfig {
   minSdkVersion 21
   targetSdkVersion 28
   testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 
   // Add config properties as build configs
   rootProject.ext.config.each { p ->
       if (p.key != 'VERSION_CODE' && p.key != 'VERSION_NAME' && p.key != 'APPLICATION_ID') {
           buildConfigField 'String', p.key, p.value
       }
   }
}

In the end of the code block, we iterate over the config key-value pairs read from properties file and add each of them as build config fields. The if condition there prevents us from adding duplicate entries into BuildConfig.java, as versionCode, versionName and applicationId are already part of build.gradle.

Now that our configs are added as build configs, let’s see how we can access them in code. Open MainActivity.kt file and add below lines in onCreate method.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   setSupportActionBar(toolbar)
 
   Log.d("VersionCode ", BuildConfig.VERSION_CODE.toString())
   Log.d("VersionName ", BuildConfig.VERSION_NAME)
   Log.d("Base URL ", BuildConfig.BASE_URL)
   Log.d("API Key ", BuildConfig.ANALYTICS_API_KEY)
   Log.d("API Secret ", BuildConfig.ANALYTICS_API_SECRET)
}

Select Build Variants tab from side bar and select devDebug as the variant as shown in the image below.

Build_variants

Run the app to see the logs as shown below.

2019-01-23 23:29:43.829 2245-2245/com.places.android.dev D/VersionCode:: 1
2019-01-23 23:29:43.829 2245-2245/com.places.android.dev D/VersionName:: 0.1
2019-01-23 23:29:43.832 2245-2245/com.places.android.dev D/Base URL:: http://api.places.com/dev/
2019-01-23 23:29:43.832 2245-2245/com.places.android.dev D/API Key:: 783fa804f48d2952c22bf6653ce4474f
2019-01-23 23:29:43.832 2245-2245/com.places.android.dev D/API Secret:: f93754758b5b7f242f89b8d38223e836

These output values correspond to the values we have configured for the dev.properties file. Try selecting other variants from Build Variant menu and observe the output. We can also observe that, once these builds are run, three separate apps co-existing in the device/emulator.

Build Variant menu and observe the output

Observe the icons and app names. They are exactly the same. As we have different application IDs for each of the flavors, we see three installed apps. Let’s address the app name next.

Let’s name our app based on the flavor. That is, Places(Dev), Places(Stg) and Places for dev, stg, and prod flavors respectively. This can be done in various ways.

By default in Android, the app name is specified as a string resource in AndroidManifest.xml file like android:label=”@string/app_name” under application tag. Here, the resource app_name is actually defined in the localizable string resource file named strings.xml, under res/values directory.

Renaming App – Method 1

In this method, we can create flavor specific directories under app/src directory and place res/values/strings.xml file under each of them. Each strings.xml file has an app_name value defined as shown in below image.

strings_xml

In this case, there will be four strings.xml files. Three from flavor specific directories and one under main/res/values/ directory. While building, strings.xml in current flavor directory is given the first preference and next main/res/values/strings.xml will be treated. In the process, duplicates will be eliminated by keeping those coming from strings.xml under current flavor directory and discarding the same resources from main/res/values/strings.xml.

Renaming App – Method 2

In this method, we can specify the app name as a string resource in app level build.gradle file. We do this in the productFlavors block we defined earlier as shown below.

productFlavors {
   dev {
       resValue "string", "app_name", "Places(Dev)"
   }
   stg {
       resValue "string", "app_name", "Places(Stg)"
   }
   prod {
       resValue "string", "app_name", "Places"
   }
}

Note, however that, duplicate resource names cause a build failure in this method. We have to make sure to remove the duplicate entry of app_name from main/res/values/strings.xml file.

This method is convenient if we don’t need a way to change the app name externally, via a config (.properties) file.

Renaming App – Method 3

In this method, we specify the app name in our flavor specific config (.properties) files so that we can handle name change without changing the code. This is the approach we proceed with in this article.

Add a new property called APP_NAME in dev.properties, stg.properties and prod.properties files, as shown below.

For dev,

# App name
APP_NAME = "Places(Dev)"

For stg,

# App name
APP_NAME = "Places(Stg)"

For prod,

# App name
APP_NAME = "Places"

In app level build.gradle file let’s read this value and add it as a string resource dynamically as shown below. We just modify config iteration block within defaultConfig by checking for APP_NAME key and adding it as a string resource, if found.

// Add config properties as build configs
rootProject.ext.config.each { p ->
   if (p.key == 'APP_NAME') {
       resValue 'string', p.key.toLowerCase(), p.value.replace(""", "")
   }
   else if (p.key != 'VERSION_CODE' && p.key != 'VERSION_NAME' && p.key != 'APPLICATION_ID') {
       buildConfigField 'String', p.key, p.value
   }
}

Notice the resValue syntax for adding a string resource.

resValue ‘string’, p.key.toLowerCase(), p.value

The type should be string and NOT String. We are also converting key to lower case to make it app_name to keep consistency in the naming in resource xmls.

Also, duplicate resource names cause a build failure in this method as well. We have to make sure to remove the duplicate entry of app_name from main/res/values/strings.xml file.

After following one of the three methods mentioned above, if we now run all the flavors, below is how it would look like.

App_icons

Now, let’s see how we can provide separate icons for each of the flavors.

When we define flavors in build.gradle, the build system searches for resources in the flavor specific directories first. If flavor directories are not found, then the search happens in main directory under app/src. Let’s start by creating flavor specific directories in side app/src directory as shown in below image.

Flavor_dir

Flavors_project

As we are interested in adding icons, let’s create a res directory under each flavor directory containing different dimension directories within as shown below. Each of the dimension directories, in-turn contain respective icon files.

Flavored_icons

Next, delete mipmap-xxxx directories under src/main/res directory, as we already provided icons in specific flavor directories. Change android:icon and android:roundIcon properties in AndroidManifest.xml file as shown below.

 <application
       android:icon="@mipmap/icon"
       android:roundIcon="@mipmap/icon_round"

Note that, icon file names specified in AndroidManifest.xml should match the actual icon files added in mipmap directories.

Build and run the application with all variants one by one. The respective apps now will have flavored icons as shown below.

Final_icons

We can also include other bespoke resources required within flavor specific directories, if these resources require to be different per flavor. One such example is the google-services.json file we require to integrate google APIs into our app development. If we happen to maintain separate google-services.json file per configuration, we can keep them in these flavor directories and at build time, these will be referenced automatically based on the build variant selected for build.

In this article, we spoke about Build Configurations for Android, in the next article of this series, we will talk about creating Build Configurations for iOS. Stay tuned.

Read More