Sets up multi-environment configuration (dev/staging/prod) in a Kotlin Multiplatform project using BuildKonfig. Covers: environment-specific BuildKonfig values, Android product flavors wired to BuildKonfig, a shared AppConfig object in commonMain, secret management, and switching environments at build time. Assumes the project was scaffolded with kmp-feature-scaffold.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-flavor-environment --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Kmp Flavor Environment?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-flavor-environment)More formats (shields.io, HTML) on the badges page.
---
name: kmp-flavor-environment
description: >
Sets up multi-environment configuration (dev/staging/prod) in a Kotlin
Multiplatform project using BuildKonfig. Covers: environment-specific
BuildKonfig values, Android product flavors wired to BuildKonfig, a shared
AppConfig object in commonMain, secret management, and switching environments
at build time. Assumes the project was scaffolded with
kmp-feature-scaffold.
license: Apache-2.0
metadata:
author: kmp-agent-skills
last-updated: '2026-06-21'
keywords:
- BuildKonfig
- multi-environment
- product flavors
- dev staging prod
- KMP config
- Kotlin Multiplatform
- secrets
- AppConfig
---
## When to Use This Skill
Use when you need to:
- Add dev / staging / prod environment configs to a KMP project
- Wire Android product flavors to BuildKonfig values (`BASE_URL`, `API_KEY`, `DEBUG`)
- Inject secrets at build time from `local.properties` or CI env vars
- Access environment config from `commonMain` without platform-specific code
**Requires:** `kmp-feature-scaffold` project structure.
**Trigger keywords:** dev/staging/prod, environment config, BuildKonfig, product flavors,
API key secrets, build variants, AppConfig, env switching, multi-environment,
staging URL, API endpoint config, debug config, production config, environment variable,
build type config, switch API URL, hide API keys, config per environment.
**Freshness rule:** BuildKonfig plugin versions and AGP product flavor APIs change — recheck
the BuildKonfig repo and version catalog before upgrading.
---
## Recommendation First
Default to **BuildKonfig + `AppConfig` sealed class + `local.properties` for secrets**.
Why:
- BuildKonfig generates a `BuildKonfig` object in `commonMain` — accessible from shared code
without expect/actual boilerplate
- wrapping it in a typed `AppConfig` sealed class keeps route handlers and services decoupled
from raw string constants
- secrets stay in `local.properties` (gitignored) and CI injects them via environment variables
Use expect/actual environment injection only when BuildKonfig does not support a specific target.
---
## Overview
BuildKonfig generates a `BuildKonfig` object in `commonMain` — the KMP equivalent
of Android `BuildConfig`. This skill layers environments on top:
```
:androidApp
Build variants:
devDebug, devRelease
stagingDebug, stagingRelease
prodDebug, prodRelease
```
All variants produce a `BuildKonfig` object in commonMain with constants like
`BASE_URL`, `DEBUG`, `ENVIRONMENT`, which all KMP modules can consume.
---
## Prerequisites
- Project scaffolded with `kmp-feature-scaffold`
- `:androidApp` applies `GROUP_ID.android.app` convention plugin (already includes BuildKonfig)
- `buildkonfig = "0.22.0"` in `libs.versions.toml`
---
## Step 1: Update `:androidApp/build.gradle.kts`
```kotlin
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
plugins {
id("GROUP_ID.android.app")
}
android {
// Define flavors
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
}
create("prod") {
dimension = "environment"
}
}
}
buildkonfig {
packageName = "GROUP_ID"
// Default config (required — used as base, overridden by flavor)
// APP_VERSION reads from gradle.properties — the single source of truth for the version
defaultConfigs {
buildConfigField(STRING, "APP_VERSION", project.property("VERSION_NAME") as String)
buildConfigField(STRING, "BASE_URL", "https://api.example.com")
buildConfigField(STRING, "ENVIRONMENT", "prod")
buildConfigField(BOOLEAN, "DEBUG", "false")
buildConfigField(BOOLEAN, "ENABLE_LOGGING","false")
}
// Dev flavor overrides
defaultConfigs("dev") {
buildConfigField(STRING, "BASE_URL", "https://dev-api.example.com")
buildConfigField(STRING, "ENVIRONMENT", "dev")
buildConfigField(BOOLEAN, "DEBUG", "true")
buildConfigField(BOOLEAN, "ENABLE_LOGGING", "true")
}
// Staging flavor overrides
defaultConfigs("staging") {
buildConfigField(STRING, "BASE_URL", "https://staging-api.example.com")
buildConfigField(STRING, "ENVIRONMENT", "staging")
buildConfigField(BOOLEAN, "DEBUG", "false")
buildConfigField(BOOLEAN, "ENABLE_LOGGING", "true")
}
// prod: uses defaultConfigs (no override needed)
// Per-target overrides (optional — for platform-specific values)
targetConfigs {
// android flavor-specific values can also be set here
}
}
```
---
## Step 2: Using BuildKonfig in commonMain
BuildKonfig generates:
```kotlin
// auto-generated in commonMain by the BuildKonfig plugin:
package GROUP_ID
internal object BuildKonfig {
val APP_VERSION: String = /* value from gradle.properties VERSION_NAME */
val BASE_URL: String = /* value from active flavor/build type */
val ENVIRONMENT: String = /* "dev" | "staging" | "prod" */
val DEBUG: Boolean = /* true | false */
val ENABLE_LOGGING: Boolean = /* true | false */
}
```
Create a public wrapper `src/commonMain/kotlin/GROUP_ID/core/config/AppConfig.kt`:
```kotlin
package GROUP_ID.core.config
/**
* Public facade over [BuildKonfig].
*
* Feature modules reference AppConfig, not BuildKonfig directly,
* so the generated class stays internal.
*/
object AppConfig {
val versionName: String get() = BuildKonfig.APP_VERSION
val baseUrl: String get() = BuildKonfig.BASE_URL
val environment: String get() = BuildKonfig.ENVIRONMENT
val isDebug: Boolean get() = BuildKonfig.DEBUG
val enableLogging: Boolean get() = BuildKonfig.ENABLE_LOGGING
val isDev: Boolean get() = environment == "dev"
val isStaging: Boolean get() = environment == "staging"
val isProd: Boolean get() = environment == "prod"
}
```
Usage in any commonMain module:
```kotlin
// In network layer
createHttpClient(
baseUrl = AppConfig.baseUrl,
enableLogging = AppConfig.enableLogging,
)
// In a ViewModel
if (AppConfig.isDev) {
showDeveloperMenu()
}
```
---
## Step 3: Secrets management
**Never commit secrets to source control.** Inject at build time via `local.properties` or CI environment variables.
Add to `local.properties` (git-ignored):
```properties
dev.api_key=abc123
staging.api_key=def456
prod.api_key=ghi789
```
Read in `build.gradle.kts`:
```kotlin
import java.util.Properties
val localProps = Properties().apply {
val f = rootProject.file("local.properties")
if (f.exists()) load(f.inputStream())
}
buildkonfig {
packageName = "GROUP_ID"
defaultConfigs {
buildConfigField(STRING, "BASE_URL", "https://api.example.com")
buildConfigField(STRING, "API_KEY", "") // empty default
}
defaultConfigs("dev") {
buildConfigField(STRING, "BASE_URL", "https://dev-api.example.com")
buildConfigField(STRING, "API_KEY",
localProps.getProperty("dev.api_key", "")
)
}
defaultConfigs("staging") {
buildConfigField(STRING, "API_KEY",
localProps.getProperty("staging.api_key",
System.getenv("STAGING_API_KEY") ?: "")
)
}
defaultConfigs("prod") {
buildConfigField(STRING, "API_KEY",
localProps.getProperty("prod.api_key",
System.getenv("PROD_API_KEY") ?: "")
)
}
}
```
In CI (GitHub Actions), inject via environment variables:
```yaml
- name: Build release
run: ./gradlew assembleProdRelease
env:
PROD_API_KEY: ${{ secrets.PROD_API_KEY }}
```
---
## Step 4: Desktop and Web environments
Desktop and Web don't have Android flavors. Use Gradle project properties or environment variables instead:
In the Desktop or Web app's `build.gradle.kts`:
```kotlin
val env = project.findProperty("env")?.toString() ?: "dev"
buildkonfig {
packageName = "GROUP_ID"
defaultConfigs {
buildConfigField(STRING, "BASE_URL", "https://api.example.com")
buildConfigField(STRING, "ENVIRONMENT", "prod")
buildConfigField(BOOLEAN, "DEBUG", "false")
}
defaultConfigs("dev") {
buildConfigField(STRING, "BASE_URL", "https://dev-api.example.com")
buildConfigField(STRING, "ENVIRONMENT", "dev")
buildConfigField(BOOLEAN, "DEBUG", "true")
}
}
```
Run with: `./gradlew :desktopApp:run -Penv=dev`
---
## Step 5: iOS environments
iOS doesn't use Gradle flavors. Use a pre-build script in Xcode or a build phase to select a Gradle property:
In Xcode Build Phases → Run Script:
```bash
cd "$SRCROOT/.."
ENVIRONMENT="${BUILD_CONFIGURATION}" # Debug/Release/Staging
if [ "$ENVIRONMENT" = "Staging" ]; then
./gradlew :shared:compileKotlinIosArm64 -Penv=staging
elif [ "$ENVIRONMENT" = "Debug" ]; then
./gradlew :shared:compileKotlinIosArm64 -Penv=dev
else
./gradlew :shared:compileKotlinIosArm64 -Penv=prod
fi
```
---
## Step 6: Build variant quick reference
| Variant | Command | BASE_URL | DEBUG | LOGGING |
|---|---|---|---|---|
| Dev Debug | `./gradlew assembleDevDebug` | dev API | true | true |
| Dev Release | `./gradlew assembleDevRelease` | dev API | false | true |
| Staging Debug | `./gradlew assembleStagingDebug` | staging API | false | true |
| Staging Release | `./gradlew assembleStagingRelease` | staging API | false | true |
| Prod Release | `./gradlew assembleProdRelease` | prod API | false | false |
---
## Guidelines
- Keep `BuildKonfig` internal — expose only via `AppConfig` (public wrapper)
- Never hardcode `BASE_URL` or API keys as string literals in source
- Always provide a `defaultConfigs` block — BuildKonfig requires it even when all values are overridden
- Add `local.properties` to `.gitignore` — it should never be committed
- For CI, prefer environment variables over Gradle properties for secrets (env vars are masked in logs)
- Use `AppConfig.isDebug` guards instead of `BuildConfig.DEBUG` — stays portable across platforms
---
## Verification
1. `./gradlew assembleDevDebug` — dev variant builds, `BuildKonfig.BASE_URL` = dev URL
2. `./gradlew assembleProdRelease` — prod variant builds, `BuildKonfig.DEBUG` = false
3. `./gradlew :androidApp:generateDevDebugBuildKonfig` — generated file contains expected values
4. Check `build/generated/buildkonfig/commonMain/` for the generated `BuildKonfig.kt`
---
## Testing
```kotlin
// BuildKonfig generates AppConfig at compile time — wrap it behind an interface so tests
// can inject a fake without depending on a specific build variant
interface AppEnvironment {
val baseUrl: String
val isDebug: Boolean
val environment: String
}
class FakeAppEnvironment(
override val baseUrl: String = "https://dev.example.com",
override val isDebug: Boolean = true,
override val environment: String = "dev",
) : AppEnvironment
@Test fun `dev url is used when environment is dev`() = runTest {
val env = FakeAppEnvironment(baseUrl = "https://dev.example.com", environment = "dev")
val repo = UserRepositoryImpl(buildClient(env.baseUrl), FakeUserDao())
assertTrue(env.baseUrl.contains("dev"))
}
@Test fun `debug flag is false in prod environment`() {
val env = FakeAppEnvironment(baseUrl = "https://api.example.com", isDebug = false, environment = "prod")
assertFalse(env.isDebug)
assertEquals("prod", env.environment)
}
@Test fun `staging and prod base urls are different`() {
val staging = FakeAppEnvironment(baseUrl = "https://staging.example.com", environment = "staging")
val prod = FakeAppEnvironment(baseUrl = "https://api.example.com", environment = "prod")
assertNotEquals(staging.baseUrl, prod.baseUrl)
}
```
---
## Common Anti-Patterns
- committing secrets to `gradle.properties` or `build.gradle.kts` — use `local.properties` and CI secrets
- accessing `BuildKonfig` constants directly in feature code — wrap in `AppConfig` sealed class first
- defining a fourth "test" flavor — use a build type override or test-specific `local.properties` instead
- using BuildKonfig for feature flags — it is for environment config, not dynamic features
- forgetting to add `local.properties` to `.gitignore` — leaks API keys if pushed
If CI builds fail on missing secrets, verify the GitHub Actions `env:` block injects all required keys.
---
## Related Skills
- `kmp-feature-scaffold` — `build-logic` convention plugins where `BuildKonfig` is applied
- `kmp-ci-github-actions` — CI secret injection via `env:` that feeds BuildKonfig values
- `kmp-network-layer` — consumes `BuildKonfig.BASE_URL` and `BuildKonfig.DEBUG`
- `kmp-logging` — consumes `BuildKonfig.DEBUG` to enable verbose log writers
---
## Output Style
When asked about environment config or BuildKonfig, respond in this order:
1. recommendation (BuildKonfig default, with local.properties for secrets)
2. code snippet (the `build.gradle.kts` block and the AppConfig wrapper)
3. why that approach is preferred
4. main alternative (manual expect/actual, environment-injected constants)
Keep the snippet focused on one flavor. Map to the user's actual base URL and environment names when provided.
---
## Changelog
| Date | Change |
|---|---|
| 2026-06-21 | **Improved** — `APP_VERSION` added to `defaultConfigs` block (reads from `gradle.properties VERSION_NAME`); `AppConfig.versionName` exposed in the public facade. |
| 2026-06-06 | Initial release. |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!