Skip to content

Managing configurations for the different environments (eg: staging, prod) in Kotlin Multiplatform

Updated: at 10:24 PM

When we are working on mobile/web app, it is common to have different configurations like different API endpoints, different logging setups and other things for different environments like staging, QA, and production. In Android, we manage those configurations with the product flavors by providing different Build Config field values for different flavors.

But when working with Kotlin multiplatform there is not a straightforward way to do the same but with some little configuration and using the BuildKonfig library by yshrsmz we can achieve a similar setup.

Project Layout

/
..
├── android
│    └── build.gradle.kts
├── ios
│    ├── ios/iosApp.xcodeproj
│    ..
├── shared
│    └── build.gradle.kts
└── gradle
     └── libs.versions.toml

Steps

  1. Apply buildkonfig plugin.

    ## gradle/libs.versions.toml
    
    [versions]
    buildkonfig = "0.15.1"
    
    [plugins]
    buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" }
    
    // build.gradle.kts
    
    plugins{
      ...
      alias(libs.plugins.buildkonfig) apply false
    }
    
    // shared/build.gradle.kts
    
    plugins{
      alias(libs.plugins.buildkonfig)
    }
    
  2. Create Android build flavors

    Here we are creating 3 flavors (i.e. dev, staging & prod) with variant dimensions

    // android/build.gradle.kts
    
    android {
      ...
    
      flavorDimensions.add("variant")
    
      productFlavors {
        create("dev") {
          dimension = "variant"
          isDefault = true
          applicationIdSuffix = ".dev"
          resValue("string", "app_name", "Config Sample Dev")
        }
    
        create("staging") {
          dimension = "variant"
          applicationIdSuffix = ".staging"
          resValue("string", "app_name", "Config Sample Staging")
        }
    
        create("prod") {
          dimension = "variant"
        }
      }
    }
    
  3. Setup the flavors and schema for IOS

    • Open your IOS project in Xcode, select your Project and Info tab and remove existing configurations (they are Debug and Release). Existing configurations

      Fig. Existing configurations
    • Add new configurations, two for each flavour. One for the debug and another for the release build. After adding configurations

      Fig. New configurations
    • Change the bundle identifier for each configuration Change bundle identifiers

      Fig. New bundle identifiers
    • Change the Product name for each configuration Product names

      Fig. New product names
    • Since we have changed the default configurations, Kotlin doesn’t know whether to build the binary for release or debug configurations. We have to add KOTLIN_FRAMEWORK_BUILD_TYPE corresponding to the configuration as user settings. KOTLIN_FRAMEWORK_BUILD_TYPE

      Fig. exposing KOTLIN_FRAMEWORK_BUILD_TYPE
    • Delete the existing schemas and the new 6 schemas that correspond to the 6 configurations. Manage schemas Manage schemas

      Fig. New schemas

      Now open each of the schemas and do the following for each:

      • Choose the corresponding configuration
      • Uncheck the Debug executable checkbox for release schemas
      Sample Config Dev Debug
      Sample Config Dev Release
       KOTLIN_FRAMEWORK_BUILD_TYPE  KOTLIN_FRAMEWORK_BUILD_TYPE
    • Now let’s expose the variant to the shared KMP module so we can generate different build config depending on the variants. Add a “VARIANT” user defined setting Variant user defined setting

      Fig. Exposing the variant as user defined settings
  4. Buildkonfig setup with the variants. Now let’s go back to the shared module’s build.gradle.kts and do the rest of the Buildkonfig setup.

       // shared/build.gradle.kts
    
      project.extra.set("buildkonfig.flavor", currentBuildVariant())
    
      buildkonfig {
        packageName = "me.sujanpoudel.kmp.config.sample"
        objectName = "SampleConfig"
        exposeObjectWithName = "SampleConfig"
    
        defaultConfigs {
          buildConfigField(FieldSpec.Type.STRING, "variant", "dev")
          buildConfigField(FieldSpec.Type.STRING, "apiEndPoint", "https://dev.example.com")
        }
    
        defaultConfigs("dev") {
          buildConfigField(FieldSpec.Type.STRING, "variant", "dev")
          buildConfigField(FieldSpec.Type.STRING, "apiEndPoint", "https://dev.example.com")
        }
    
        defaultConfigs("staging") {
          buildConfigField(FieldSpec.Type.STRING, "variant", "staging")
          buildConfigField(FieldSpec.Type.STRING, "apiEndPoint", "https://staging.example.com")
        }
    
        defaultConfigs("prod") {
          buildConfigField(FieldSpec.Type.STRING, "variant", "prod")
          buildConfigField(FieldSpec.Type.STRING, "apiEndPoint", "https://example.com")
        }
      }

    A few key things here are:

    • we are setting buildkonfig.flavor property at build time.
    • currentBuildVariant() determines the current Build Variant, which can be for the android’s variant or ios’s variant.
    • dev is the default variant

    Body for the currentBuildVariant()

     // shared/build.gradle.kts
    
     fun Project.getAndroidBuildVariantOrNull(): String? {
       val variants = setOf("dev", "prod", "staging")
       val taskRequestsStr = gradle.startParameter.taskRequests.toString()
       val pattern: Pattern = if (taskRequestsStr.contains("assemble")) {
         Pattern.compile("assemble(\\w+)(Release|Debug)")
       } else {
         Pattern.compile("bundle(\\w+)(Release|Debug)")
       }
    
       val matcher = pattern.matcher(taskRequestsStr)
       val variant = if (matcher.find()) matcher.group(1).lowercase() else null
       return if (variant in variants) {
         variant
       } else {
         null
       }
     }
    
     private fun Project.currentBuildVariant(): String {
       val variants = setOf("dev", "prod", "staging")
       return getAndroidBuildVariantOrNull()
         ?: System.getenv()["VARIANT"]
           .toString()
           .takeIf { it in variants } ?: dev
     }
    

Now everything is in place and you should be able to choose the build variant from Android Studio or different schemas from Xcode and the correct build config object should be generated. To change the variant for the other targets like for desktop or for the js you have to set the environment variable.

Different variant for desktop

With a little bit more work we can load the build configs from a property file. You can check the sample app for that.

buildkonfig {
  packageName = "me.sujanpoudel.kmp.config.sample"
  objectName = "SampleConfig"

  defaultConfigs {
    field("variant", dev)
    configsFromProperties("dev.sample.properties")
  }

  defaultConfigs(dev) {
    field("variant", dev)
    configsFromProperties("dev.sample.properties")
  }

  defaultConfigs(staging) {
    field("variant", staging)
    configsFromProperties("staging.sample.properties")
  }

  defaultConfigs(prod) {
    field("variant", prod)
    configsFromProperties("prod.sample.properties")
  }
}

You can find the sample app at https://github.com/psuzn/kmp-config-sample.

If you find any typos or problems on this blog, please create an issue here.