# Dynamic Environment Switching on Android

* How many times have you been asked to send a prod or staging pointed build to your quality engineering team?
    

* How many times have you had to build and install another build to test a functionality pointing to different environments?
    
* How many times have your Backend engineers wanted to test the functionality during the development on their local setup?
    

On numerous counts. Isn’t it? On every occasion, you had to start the tedious build process which will take its own sweet build time thus delaying the quality and testing turnaround time.

Not anymore! At [LazyPay](https://play.google.com/store/apps/details?id=com.citrus.citruspay), we developed an in-app flow to let anyone switch their environment without requiring new builds enhancing our developer and QA productivity. This article will help you understand the path we took and guide you accordingly in creating *One Build to Rule Them All* for your Android apps. 🍿 ☕️

### Conventional Process ✍️

In general, to support different environments, we will have a 1:1 mapping of [`productFlavor`](https://developer.android.com/reference/tools/gradle-api/4.2/com/android/build/api/dsl/ProductFlavor) the environment’s host URL. For the sake of consistency and to extract the best of Kotlin, we will be talking in terms of [Gradle’s Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) for all build-related scripts. For eg. Say, our app should support two backend environments Production and Staging. This is what our `build.gradle.kts` looks like:

```kotlin
android {
  ...
  flavorDimensions.addAll(listOf("env"))
  productFlavors {
    create("prod") {
        dimension = "env"
        buildConfigField("String", "HOST_URL", "https://prod.api.com")
        buildConfigField("String", "SOME_KEY", "prod_key") 
   }
    create("staging") {
        dimension = "env"
        buildConfigField("String", "HOST_URL", "https://staging.api.com")
        buildConfigField("String", "SOME_KEY", "staging_key")     
    }
  }
  ...
}
```

When we create `prodDebug` and `stagingDebug` apps, each will be pointing to their respective environment's `HOST_URL`. To test a feature’s behaviour on multiple environments, we will build separate apps accordingly.

### The Idea💡

On Android, when we insert any key-value pair under `buildConfigField` and compile the source, `BuildConfig.java` is auto-generated and this changes according to our `buildFlavor` defined values. For the example above, `BuildConfig.java` will look something like this for `stagingDebug`build variant:

```kotlin
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.example.app";
public static final String HOST_URL="https://staging.api.com";
public static final String SOME_KEY = "staging_key";
}
```

If we can somehow tweak this `BuildConfig.java` to accommodate the configurations for all supporting `buildFlavors` and environments, this will solve the problem for us. Unfortunately, `BuildConfig.java` is generated at compile time and cannot be dynamically changed once the build process has been completed. What if we don’t have to change this dynamically and still be able to hold the configurations for different `productFlavor`. Let’s build on top of this idea.

#### Injecting build configuration 🏗

We now understand that `BuildConfig.java` holds all the values according to the `key` type defined under `buildConfigField`. For `String` key as `HOST_URL`, it generated `public static final String HOST_URL` with its subsequent value. To allow for switching environments from one build, we would like to have all the configurations(prod/staging, etc.) available under our `BuildConfig.java`.

**Mapping Build Flavor with Configurations**

We will set up all the `buildFlavors` and their corresponding environment configurations in a way such that the auto-generated`BuildConfig.java`contains the following:

```kotlin
public static final java.util.Map<String,String> PROD_MAP = 
new java.util.HashMap() {{put("HOST_URL","https://prod.api.com"); put("SOME_KEY","prod_key")}};

public static final java.util.Map<String,String> STAGING_MAP = 
new java.util.HashMap() {{put("HOST_URL","https://staging.api.com"); put("SOME_KEY","staging_key")}};

public static final java.util.ArrayList SET_OF_FLAVORS = 
new java.util.ArrayList() {{add("prod");add("staging");}};
```

If we *can* generate the `PROD_MAP` and `STAGING_MAP`along with a `set` of supported flavors, our app will have all the information available to switch the configuration at runtime. The concept is also driven by a standard code generator like [KotlinPoet](https://github.com/square/kotlinpoet).

Before we do this step, we should also do housekeeping of our `build.gradle.kts` to move all the build configurations to one place for easy maintenance. We will create `Config.kt` inside the `buildSrc` folder for the same.

%[https://gist.github.com/reactivedroid/3b75bc7f269088d42272d149260dcad0] 

Once we have segregated the build configurations, we will now tweak our `build.gradle.kts` file to auto-generate the subsequent flavor `map` and `set` on `BuildConfig.java`.

* Under `defaultConfig`, we will ensure that all our *Keys* are available on `BuildConfig` file.
    

```kotlin
Config.keyList.forEach { key ->
buildConfigField(“String”, “KEY_$key”, “\”$key\””) 
}
// This generates the keys and writes on BuildConfig
public class BuildConfig {
  public static final String KEY_HOST_URL = "HOST_URL";
  public static final String KEY_SOME_KEY = "SOME_KEY";
}
```

* Next, to have the configurations available for each `productFlavor`*,* we will change our `build.gradle.kts` to generate `<flavor>_MAP` on `BuildConfig.java`.
    

```kotlin
applicationVariants.all {
 defaultFlavors.forEach { value -> 
  buildConfigField(
  "java.util.Map<String,String>",  // type 
  "${value.toUpperCase()}_MAP", // name
  variantFields(value)      // value          
  )            
 }
}
fun variantFields(flavor: String): String {
    val fields = variantFieldMap[flavor] // Defined in Config.kt
    val fieldsBuilder = StringBuilder("")
    fields!!.forEach { entry ->
   fieldsBuilder.append("put(\"${entry.key}\",\"${entry.value}\");")
    }
    return "new java.util.HashMap() {{$fieldsBuilder}}"
}
```

> Since [`buildConfigField`](https://developer.android.com/reference/tools/gradle-api/4.1/com/android/build/api/variant/BuildConfigField)) only takes `String` as the value type that can be written on `BuildConfig`, we have provided the `type`explicitly as `java.util.Map<String, String>` and the value as the build configurations map.

This will write `PROD_MAP` and `STAGING_MAP` on `BuildConfig` like this:

```kotlin
public static final java.util.Map<String,String> PROD_MAP = 
new java.util.HashMap() {{put("HOST_URL","https://prod.api.com"); put("SOME_KEY","prod_key")}};
public static final java.util.Map<String,String> STAGING_MAP = 
new java.util.HashMap() {{put("HOST_URL","https://staging.api.com"); put("SOME_KEY","staging_key")}};
```

* Lastly, we will also write the `productFlavors` on `BuildConfig` which app will support. This will help in showing the options to the end-user in switching the environment.
    

```kotlin
applicationVariants.all {
 buildConfigField("java.util.ArrayList<String>", "SET_OF_FLAVORS", getFlavorList())
}
fun getFlavorList(): String {
    val flavorBuilder = StringBuilder()
    defaultFlavors.forEach {
        flavorBuilder.append("add(\"$it\");")
    }
    return "new java.util.ArrayList() {{$flavorBuilder}}"
}
```

This will write the `list` of flavors on `BuildConfig`.

```kotlin
public static final java.util.ArrayList<String> SET_OF_FLAVORS = 
new java.util.ArrayList() {{add("prod");add("staging");}};
```

Once we have executed the above steps, our apps `build.gradle.kts` should look something like this:

%[https://gist.github.com/reactivedroid/2a7ba19c6af18fbecfacfe4d38661330] 

> To ensure the security of our app, for any buildType of `release` variant like `prodRelease`, we will not write other flavor configurations on `BuildConfig` and just create the `RELEASE_MAP` (line:13 on above GIST)

#### Accessing the BuildConfig 🔓

We have everything written on `BuildConfig` as required to develop the flow for dynamically switching environments. Let’s connect the final dots.

We will create an [*abstraction*](https://en.wikipedia.org/wiki/Abstraction_%28computer_science%29) over `BuildConfig` for accessing their values. If you are using [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) via [Dagger](https://dagger.dev/) or [HILT](https://developer.android.com/training/dependency-injection/hilt-android), it will help in keeping your [concrete classes](https://en.wikipedia.org/wiki/Class_%28computer_programming%29#Abstract_and_Concrete) clean enhancing the Unit Testing. Here’s what our `BuildConfigProvider` will look like this:

%[https://gist.github.com/reactivedroid/28d7f4b8b93499ebad96dd7a9c331c8d] 

`BuildConfigProviderImpl` will have the detailed implementation of all the APIs exposed via `BuildConfigProvider`.

%[https://gist.github.com/reactivedroid/efd28badd08dac80dd46dc31adee80a9] 

Now, all we will need to do is to define a [*binding*](https://developer.android.com/training/dependency-injection/hilt-android#define-bindings) for `BuildConfigProvider` such that [Dagger](https://dagger.dev/) or [HILT](https://developer.android.com/training/dependency-injection/hilt-android) knows how to provide the instance of this abstraction. Once this is set up, we will be able to [*inject*](https://developer.android.com/training/dependency-injection/hilt-android#inject-interfaces) `BuildConfigProvider` as a dependency wherever required.

```kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class AppBindingModule {
 @Binds
 abstract fun provideBuildConfigProvider(buildConfigProvider: BuildConfigProviderImpl): BuildConfigProvider
}
```

#### Change Environment w.r.t Flavor 🚀

With the above implementation in place, we were able to provide the environment-switching capability right from within the [LazyPay](https://play.google.com/store/apps/details?id=com.citrus.citruspay) app.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697539487489/e125dd36-0379-4d86-9e8f-27f6b2214847.png align="center")

[LazyPay](https://play.google.com/store/apps/details?id=com.citrus.citruspay) debug app showcasing supported environments. Build using [Jetpack Compose](https://developer.android.com/jetpack/compose) ❤️

#### Add-On: Custom Domain Support 🚀 💯

While we have developed the environment switching feature, there is one feature missing in our implementation and that is to support custom domain configuration. This also came as huge demand from our Backend and [BFF](https://samnewman.io/patterns/architectural/bff/) Developers as they wanted to dev test their APIs from their local setup. Keep reading to know how we built it.

Until now, we have provided support for all the environments where we have 1:1 mapping of `productFlavor` and its `build` environment(`HOST_URL`). We will now expand our idea to provide custom domain support.

* Since only `HOST_URL` will have to change, we will fix the `productFlavor` where other configurations remain the same and only `HOST_URL` can be edited. We will be configuring it on our `staging` flavor.
    
* We will tweak our `BuildConfigProvider` to expose the setter/getter for changing the `HOST_URL`.
    
    ```kotlin
    class BuildConfigProviderImpl : BuildConfigProvider {
        ...
     override fun setHostUrl(baseUrl: String) {
      // store in SharedPreferences
     }
     override fun getHostUrl(): String {
         val baseUrl = // get from SharedPreferences else return empty
         return baseUrl ?: getValue(BuildConfig.KEY_CS_JAVA_URL)
     }
    }
    ```
    
* Unfortunately, the app will start throwing the below error when the domain is changed.
    
    ```kotlin
    java.net.UnknownServiceException: CLEARTEXT communication to <custom_domain> not permitted by network security policy
    ```
    
* *Solving java.net.UnknownServiceException*: As all our `productFlavor` with the `HOST_URL` like `https://prod.api.com`will go via secure connections, `[clearTextTrafficPermitted](https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted)` is always set to `false` by default. This will avoid fraudulent and insecure connections to our app. But, custom domain setup will require us to tweak the `[network-security-config](https://developer.android.com/training/articles/security-config)` to allow for insecure connections. We will create separate `network-security-config` files for different `buildType`. Our `debug` variants will allow for `[clearTextTrafficPermitted](https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted)` while the release won’t.
    
    ```kotlin
    // Create debug folder and place network-security-config under there
    <network-security-config>
        <base-config cleartextTrafficPermitted="true"/>
        <debug-overrides>
            <trust-anchors>
                <certificates src="system" />
                <certificates src="user" />
            </trust-anchors>
        </debug-overrides>
    </network-security-config>
    ```
    

At LazyPay, we provided custom domain support on our `sbox` environment like this.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697539488926/9edd7200-b7d2-4a3f-b856-833d0b38507c.png align="center")

[LazyPay](https://play.google.com/store/apps/details?id=com.citrus.citruspay) debug app showcasing custom domain support. Build using [Jetpack Compose](https://developer.android.com/jetpack/compose) ❤️

> Once a user attempts to switch the domain/environment on app, it is recommended to clear any app cache restarting the app as a fresh launch.

#### Add-On: Obfuscation on Release Variant 🚀 💯

If we like to have the above feature built on a variant where [obfuscation and minification](https://developer.android.com/studio/build/shrink-code#enable) run via Proguard typically `release` variant, our apps will crash due to code obfuscation. To fix this, we will add the following rule on `proguard-rules.pro`:

```kotlin
# Not obfuscating config-maps of BuildConfig as we are accessing them via reflections
-keepclassmembers class com.citrus.citruspay.BuildConfig {
    public static final java.util.Map PROD_MAP;
    public static final java.util.Map STAGING_MAP;
}
```

### Conclusion ✅

We have provided all our learnings in building a feature that directly enhances the developer's productivity and experience. Time saved in generating different build variants is time gained for faster development and testing cycles.

As we gear up for a new age in financial technology, we are also striving hard to prepare our apps built for India scale following [Modern Android Development](https://developer.android.com/series/mad-skills), writing UI in [Jetpack Compose](https://developer.android.com/jetpack/compose), Unit Tests, and much more. At [LazyPay](https://play.google.com/store/apps/details?id=com.citrus.citruspay), we are working every day to improve not only our products but also excel in terms of how we engineer them for scale. If this excites you, come join our Experience Engineering team at PayU Credit. We are [hiring](https://jobs.eu.lever.co/payu) across all domains.

*PS: Next blog will talk about how our iOS engineering team developed a similar feature to improve their productivity. Stay tuned.* 🙌
