Jul 1, 2019
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.
There are various reasons to configure builds. A few of them are;
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.
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;
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 three files namely dev.properties, stg.properties and prod.properties in the config directory created as shown in the image below.
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;
Also, we would name our app bundle identifiers as;
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.
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
and prod.properties file.
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.
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.
The above method extracts the flavor name from one of the gradle tasks and returns. It uses a regex to do this job.
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.
Let’s open the app (module) level build.gradle and see how we can substitute the properties read earlier.
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.
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.
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.
Select Build Variants tab from side bar and select devDebug as the variant as shown in the image below.
Run the app to see the logs as shown below.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.