KEnv Config
Type-safe, schema-driven environment configuration for Kotlin Multiplatform and Android.
KEnv Config is a Gradle plugin that generates Kotlin objects from YAML schema files and environment-specific value files. It catches missing values at compile time, supports multiple environments, and provides IDE documentation through KDoc generation.
Installation
Version Catalog (TOML) — Recommended
# gradle/libs.versions.toml
[versions]
kenvConfig = "0.2.0"
[plugins]
kenvConfig = { id = "io.github.adventures92.kenv-config", version.ref = "kenvConfig" }
// build.gradle.kts
plugins {
alias(libs.plugins.kenvConfig)
}
Direct Application
// build.gradle.kts
plugins {
id("io.github.adventures92.kenv-config") version "0.2.0"
}
Quick Start
kenvConfig {
directory.set(file("kenv"))
environments.set(listOf("dev", "production"))
generatedPackageName.set("com.example.config")
}
Then access your config in code:
// Flat access after setting active environment (KMP)
EnvConfig.setActiveEnvironment("production")
val apiUrl = EnvConfig.Server.API_BASE_URL
// Or direct per-environment access
val devUrl = EnvConfig.Dev.Server.API_BASE_URL
val prodUrl = EnvConfig.Production.Server.API_BASE_URL
Documentation
- Getting Started — Full setup walkthrough
- Schema Reference — Types, scopes, groups, env file formats
- Plugin Configuration — DSL options, tasks, error messages
- Android Variant Mapping — Per-build-type code generation
- Runtime Environment Selection — KMP environment switching
- Code Generation — Generated code structure and behavior
Features
- Type-safe — Generated Kotlin objects with correct types (String, Int, Long, Double, Float, Boolean, Url)
- Compile-time validation — Missing values are caught during build, not at runtime
- Multi-environment — Dev, staging, production — as many as you need
- KMP compatible — Works in commonMain with runtime environment selection
- Android variant mapping — Automatic per-build-type code generation
- KDoc generation — Schema descriptions become IDE-visible documentation
- Groups — Organize variables into logical nested objects
- Unified directory — Single
kenv/directory for all config files - Multi-format — Environment files in
.env,.yaml,.yml, or.toml - Incremental builds — Cacheable task, skipped when inputs unchanged
Getting Started
Installation
Option 1: Version Catalog (TOML) — Recommended
Add to your gradle/libs.versions.toml:
[versions]
kenvConfig = "0.2.0"
[plugins]
kenvConfig = { id = "io.github.adventures92.kenv-config", version.ref = "kenvConfig" }
Then in your module’s build.gradle.kts:
plugins {
alias(libs.plugins.kenvConfig)
}
Option 2: Direct Plugin Application
In your module’s build.gradle.kts:
plugins {
id("io.github.adventures92.kenv-config") version "0.2.0"
}
Repository Setup
Ensure mavenCentral() and gradlePluginPortal() are in your settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
Project Setup
1. Create the kenv directory
Create a kenv/ directory in your module root:
my-app/
├── kenv/
│ ├── schema.kenv.yaml
│ ├── env.dev.env
│ ├── env.production.env
│ └── env.global.env
├── src/
└── build.gradle.kts
2. Define your schema
Create kenv/schema.kenv.yaml:
environments:
- dev
- production
variables:
DEBUG_MODE:
type: Boolean
scope: environment
description: "Enable debug logging"
groups:
server:
API_URL:
type: Url
scope: environment
description: "Backend API base URL"
API_PORT:
type: Int
scope: environment
description: "Backend API port"
app:
APP_NAME:
type: String
scope: global
description: "Application display name"
3. Create environment files
kenv/env.dev.env:
DEBUG_MODE=true
API_URL=http://localhost:8080
API_PORT=8080
kenv/env.production.env:
DEBUG_MODE=false
API_URL=https://api.myapp.com
API_PORT=443
kenv/env.global.env:
APP_NAME=My App
4. Configure the plugin
In your build.gradle.kts:
kenvConfig {
directory.set(file("kenv"))
environments.set(listOf("dev", "production"))
generatedPackageName.set("com.example.config")
}
5. Generate and use
Run the generation task:
./gradlew kenvGenerate
Then use the generated code:
import com.example.config.EnvConfig
// Set active environment (KMP approach)
EnvConfig.setActiveEnvironment("dev")
// Access values with flat syntax
println(EnvConfig.Server.API_URL) // "http://localhost:8080"
println(EnvConfig.Server.API_PORT) // 8080
println(EnvConfig.DEBUG_MODE) // true
println(EnvConfig.App.APP_NAME) // "My App"
// Or access per-environment directly
println(EnvConfig.Production.Server.API_URL) // "https://api.myapp.com"
Next Steps
- Schema Reference — All supported types and fields
- Plugin Configuration — Full DSL options
- Android Variant Mapping — Automatic per-build-type generation
- Runtime Environment Selection — KMP environment switching
Schema Reference
The schema file (schema.kenv.yaml) declares the structure of your configuration: which variables exist, their types, scopes, and documentation.
File Location
The schema must be named schema.kenv.yaml and placed in the kenv directory (default: kenv/).
Structure
environments:
- <env_name>
- <env_name>
variables:
<VARIABLE_NAME>:
type: <Type>
scope: <Scope>
description: "<optional description>"
groups:
<group_name>:
<VARIABLE_NAME>:
type: <Type>
scope: <Scope>
description: "<optional description>"
Environments
The environments list declares all environment names. Each environment must have a corresponding env file (env.<name>.<ext>).
environments:
- dev
- staging
- production
Variables
Type
| Type | Kotlin Type | Example Value |
|---|---|---|
String | String | "hello" |
Int | Int | 42 |
Long | Long | 123456789 |
Double | Double | 3.14 |
Float | Float | 2.5 |
Boolean | Boolean | true / false |
Url | String | "https://example.com" |
Scope
| Scope | Meaning | Value Source |
|---|---|---|
environment | Different per environment | env.<name>.<ext> files |
global | Same across all environments | env.global.<ext> file |
If scope is omitted, it defaults to environment.
Description
Optional. When provided, generates a KDoc comment above the property in the generated code:
variables:
API_URL:
type: Url
scope: environment
description: "Backend API base URL including protocol"
Generates:
/** Backend API base URL including protocol */
val API_URL: String = "https://api.example.com"
Special characters (*/, @) are automatically escaped in KDoc output.
Groups
Groups organize variables into nested Kotlin objects:
groups:
database:
DB_HOST:
type: String
scope: environment
DB_PORT:
type: Int
scope: global
Generates:
object Database {
val DB_HOST: String = "localhost"
val DB_PORT: Int = 5432
}
Access via: EnvConfig.Database.DB_HOST
Environment Files
Supported Formats
| Extension | Format |
|---|---|
.env | Dotenv (KEY=VALUE) |
.yaml / .yml | YAML mapping |
.toml | TOML |
Naming Convention
- Environment files:
env.<environment_name>.<ext> - Global file:
env.global.<ext>
Examples:
kenv/
├── schema.kenv.yaml
├── env.dev.env
├── env.staging.yaml
├── env.production.toml
└── env.global.env
Dotenv Format
# Comments are supported
API_URL=https://api.example.com
API_PORT=443
DEBUG_MODE=false
YAML Format
API_URL: https://api.example.com
API_PORT: 443
DEBUG_MODE: false
TOML Format
API_URL = "https://api.example.com"
API_PORT = 443
DEBUG_MODE = false
Validation Rules
- All environment-scoped variables must have a value in every env file
- All global-scoped variables must have a value in the global env file
- Type checking — values must parse as their declared type
- No defaults — the
defaultfield is not supported in v2; all values must be explicit - Undeclared variables in env files produce warnings (not errors)
Plugin Configuration
DSL Reference
The plugin is configured via the kenvConfig block in your build.gradle.kts:
kenvConfig {
// Required: list of environment names
environments.set(listOf("dev", "staging", "production"))
// Required: package name for generated code
generatedPackageName.set("com.example.config")
// Optional: directory containing schema and env files (default: "kenv/")
directory.set(file("kenv"))
// Optional: generated class name (default: "EnvConfig")
generatedClassName.set("EnvConfig")
// Optional: Android variant mapping
variantMapping {
buildType("debug") uses "dev"
buildType("release") uses "production"
}
}
Properties
directory
- Type:
DirectoryProperty - Default:
kenv/relative to the project directory - Description: The single directory containing
schema.kenv.yamland all environment files.
environments
- Type:
ListProperty<String> - Required: Yes
- Description: List of environment names to process. Each name must have a corresponding
env.<name>.<ext>file in the kenv directory.
generatedPackageName
- Type:
Property<String> - Required: Yes
- Description: Package declaration for the generated Kotlin source file. Build fails if not set.
generatedClassName
- Type:
Property<String> - Default:
"EnvConfig" - Description: Name of the generated Kotlin object class.
variantMapping
- Type:
VariantMappingDsl - Required: No
- Description: Maps Android build types or product flavors to environment names. See Android Variant Mapping.
Gradle Tasks
kenvGenerate
The default generation task. Produces multi-environment output with nested objects per environment plus flat access via setActiveEnvironment().
./gradlew kenvGenerate
Supports the -PactiveEnvironment=<name> flag to generate a single flat object for one environment:
./gradlew kenvGenerate -PactiveEnvironment=production
kenvGenerate<BuildType> (with variant mapping)
When variant mapping is configured, per-variant tasks are registered:
./gradlew kenvGenerateDebug # Generates flat config using "dev" environment
./gradlew kenvGenerateRelease # Generates flat config using "production" environment
Output Location
| Mode | Output Directory |
|---|---|
| Default (multi-env) | build/generated/kenv/commonMain/kotlin/ |
Active environment (-P flag) | build/generated/kenv/commonMain/kotlin/ |
| Variant mapping (debug) | build/generated/kenv/debug/kotlin/ |
| Variant mapping (release) | build/generated/kenv/release/kotlin/ |
Error Messages
| Condition | Error |
|---|---|
generatedPackageName not set | "generatedPackageName is required. Set it in the kenvConfig block." |
| Schema file missing | "Schema file not found: expected schema.kenv.yaml in <dir>" |
Variable has default field | "Variable '<name>' contains a 'default' field which is not supported in v2..." |
| Missing env-scoped variable | "Missing required variable '<name>' in environment '<env>'" |
| Missing global variable | "Missing global variable '<name>': define in env.global.<ext>" |
| Type mismatch | "Type mismatch for '<name>' in environment '<env>': expected <type>, got '<value>'" |
| Invalid variant mapping | "Variant mapping error: build type '<name>' maps to environment '<env>' which is not in declared environments: [...]" |
Gitignore Advisory
The plugin logs a warning if no .gitignore exists in the kenv directory:
KEnv: No .gitignore found in <dir>. Consider adding one with patterns like:
env.production.*
env.global.*
This is advisory only — the plugin never creates or modifies files.
Android Variant Mapping
Variant mapping automatically generates environment-specific configuration for each Android build type or product flavor. This eliminates the need for -PactiveEnvironment flags or runtime environment selection on Android.
How It Works
- You declare which build type maps to which environment
- The plugin generates a flat
EnvConfigobject per variant - Each variant’s generated code is wired into its source set
- At compile time, the correct environment values are baked in
Configuration
kenvConfig {
directory.set(file("kenv"))
environments.set(listOf("dev", "staging", "production"))
generatedPackageName.set("com.example.config")
variantMapping {
buildType("debug") uses "dev"
buildType("release") uses "production"
}
}
Build Type Mapping
variantMapping {
buildType("debug") uses "dev"
buildType("release") uses "production"
}
Product Flavor Mapping
variantMapping {
flavor("free") uses "dev"
flavor("paid") uses "production"
}
Combined
variantMapping {
buildType("debug") uses "dev"
buildType("release") uses "production"
flavor("staging") uses "staging"
}
Generated Output
With variant mapping, the generated code is a flat object — no nested environment objects:
// Generated for debug build type (using "dev" environment)
package com.example.config
object EnvConfig {
val APP_VERSION: String = "2.0.0"
val DEBUG_MODE: Boolean = true
object Server {
val API_BASE_URL: String = "http://localhost:8080"
val API_PORT: Int = 8080
}
}
Usage in Code
With variant mapping, access is straightforward:
// No environment prefix needed — values are resolved at compile time
val url = EnvConfig.Server.API_BASE_URL
val port = EnvConfig.Server.API_PORT
val debug = EnvConfig.DEBUG_MODE
The same code compiles differently for debug vs release builds, with each getting the correct environment values.
Source Set Wiring
For KMP Android projects, you may need to manually wire the generated source directory:
kotlin {
sourceSets {
androidMain {
kotlin.srcDir(layout.buildDirectory.dir("generated/kenv/debug/kotlin"))
}
}
}
// Wire task dependencies
tasks.matching { it.name == "compileDebugKotlinAndroid" }.configureEach {
dependsOn("kenvGenerateDebug")
}
tasks.matching { it.name == "compileReleaseKotlinAndroid" }.configureEach {
dependsOn("kenvGenerateRelease")
}
Validation
The plugin validates that all mapped environments exist in the declared environments list:
// This will fail at configuration time:
kenvConfig {
environments.set(listOf("dev", "production"))
variantMapping {
buildType("debug") uses "staging" // Error: "staging" not in environments
}
}
Error message:
Variant mapping error: build type 'debug' maps to environment 'staging'
which is not in declared environments: [dev, production]
When to Use Variant Mapping vs Runtime Selection
| Approach | Best For |
|---|---|
| Variant mapping | Android-only projects where build types align with environments |
| Runtime selection | KMP projects, dynamic environment switching, server-driven config |
| Direct access | When you need to compare values across environments |
See Runtime Environment Selection for the KMP approach.
Runtime Environment Selection
In Kotlin Multiplatform projects where Android build variants aren’t available (or when you need dynamic environment switching), the generated code provides a runtime selection API.
How It Works
The generated EnvConfig object includes:
setActiveEnvironment(name)— Sets which environment’s values are returned by flat accessors- Flat property accessors — Environment-scoped properties that delegate to the active environment
- Per-environment nested objects — Direct access to any environment’s values without setting an active one
Setting the Active Environment
Call setActiveEnvironment() once at app startup:
// In your app initialization (Application.onCreate, main(), etc.)
EnvConfig.setActiveEnvironment("production")
After this call, all environment-scoped flat accessors return the production values:
EnvConfig.Server.API_BASE_URL // → "https://api.example.com"
EnvConfig.Server.API_PORT // → 443
EnvConfig.DEBUG_MODE // → false
Switching at Runtime
You can call setActiveEnvironment() multiple times to switch environments:
// Switch to dev
EnvConfig.setActiveEnvironment("dev")
println(EnvConfig.Server.API_BASE_URL) // → "http://localhost:8080"
// Switch to production
EnvConfig.setActiveEnvironment("production")
println(EnvConfig.Server.API_BASE_URL) // → "https://api.example.com"
Checking the Active Environment
val current = EnvConfig.activeEnvironment // "production" or null if not set
Error Handling
If you access a flat property without setting an active environment:
// Throws IllegalStateException:
// "Active environment not set. Call EnvConfig.setActiveEnvironment() first."
val url = EnvConfig.Server.API_BASE_URL
If you pass an invalid environment name:
// Throws IllegalArgumentException:
// "Invalid environment 'staging'. Valid environments: dev, production"
EnvConfig.setActiveEnvironment("staging")
Direct Per-Environment Access
You can always access any environment’s values directly without setting an active environment:
// No setActiveEnvironment() needed
val devUrl = EnvConfig.Dev.Server.API_BASE_URL
val prodUrl = EnvConfig.Production.Server.API_BASE_URL
This is useful when you need to compare values across environments or display multiple environments simultaneously.
Global Variables
Global-scoped variables are always accessible regardless of the active environment:
// These work without setActiveEnvironment()
val appName = EnvConfig.Identity.APP_NAME
val version = EnvConfig.APP_VERSION
KMP Usage Pattern
A typical KMP app initialization:
// commonMain
fun initApp(environment: String) {
EnvConfig.setActiveEnvironment(environment)
// Now all flat accessors work
}
// androidMain
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
initApp(if (BuildConfig.DEBUG) "dev" else "production")
// ...
}
}
// iosMain
fun MainViewController() = ComposeUIViewController {
initApp("production") // Or read from Info.plist, launch args, etc.
App()
}
Thread Safety
The active environment is stored in a simple mutable variable. If you need thread-safe access in a concurrent environment, synchronize calls to setActiveEnvironment() or set it once during initialization before any concurrent reads.
Comparison with Variant Mapping
| Feature | Runtime Selection | Variant Mapping |
|---|---|---|
| Platform | All (KMP) | Android only |
| Resolution time | Runtime | Compile time |
| Switching | Dynamic | Fixed per build |
| Code size | All environments included | Only one environment |
| Use case | KMP, dynamic config | Android build types |
For Android-only projects where environments align with build types, prefer variant mapping for smaller APK size and compile-time guarantees.
Code Generation
This page describes the structure and behavior of the generated Kotlin code.
Generated File Location
The generated file is placed at:
build/generated/kenv/commonMain/kotlin/<package_path>/<ClassName>.kt
For example, with generatedPackageName.set("com.example.config") and default class name:
build/generated/kenv/commonMain/kotlin/com/example/config/EnvConfig.kt
Generated Structure
Multi-Environment Mode (Default)
When no active environment is specified, the generated code includes:
- Runtime selection API —
setActiveEnvironment()and flat accessors - Global properties — Top-level vals for global-scoped variables
- Per-environment nested objects —
EnvConfig.Dev,EnvConfig.Production, etc.
package com.example.config
object EnvConfig {
// --- Runtime selection ---
private var _activeEnvironment: String? = null
fun setActiveEnvironment(environment: String) { ... }
val activeEnvironment: String? get() = _activeEnvironment
// --- Global properties (always accessible) ---
/** Application version */
val APP_VERSION: String = "2.0.0"
// --- Flat accessors (require setActiveEnvironment) ---
/** Enable debug mode */
val DEBUG_MODE: Boolean
get() = when (_activeEnvironment) {
"dev" -> true
"production" -> false
else -> throw IllegalStateException(...)
}
// --- Groups with flat accessors ---
object Server {
/** API base URL */
val API_BASE_URL: String
get() = when (_activeEnvironment) {
"dev" -> "http://localhost:8080"
"production" -> "https://api.example.com"
else -> throw IllegalStateException(...)
}
}
// --- Per-environment objects (direct access) ---
object Dev {
val DEBUG_MODE: Boolean = true
object Server {
val API_BASE_URL: String = "http://localhost:8080"
}
}
object Production {
val DEBUG_MODE: Boolean = false
object Server {
val API_BASE_URL: String = "https://api.example.com"
}
}
}
Active Environment Mode (Flat)
When using -PactiveEnvironment=dev or variant mapping, the generated code is a simple flat object:
package com.example.config
object EnvConfig {
/** Application version */
val APP_VERSION: String = "2.0.0"
/** Enable debug mode */
val DEBUG_MODE: Boolean = true
object Server {
/** API base URL */
val API_BASE_URL: String = "http://localhost:8080"
}
}
KDoc Generation
Every variable with a description field in the schema gets a KDoc comment:
# Schema
API_URL:
type: Url
scope: environment
description: "Backend API base URL including protocol"
// Generated
/** Backend API base URL including protocol */
val API_URL: String = "https://api.example.com"
Special Character Escaping
KDoc-special characters are automatically escaped:
| Character | Escaped To |
|---|---|
*/ | */ |
@ | @ |
Type Mapping
| Schema Type | Kotlin Type | Generated Literal |
|---|---|---|
String | String | "value" |
Int | Int | 42 |
Long | Long | 123456789L |
Double | Double | 3.14 |
Float | Float | 2.5f |
Boolean | Boolean | true / false |
Url | String | "https://..." |
String Escaping
Special characters in string values are properly escaped:
| Character | Escaped To |
|---|---|
\ | \\ |
" | \" |
\n | \\n |
\r | \\r |
\t | \\t |
$ | \\$ |
Package Declaration
The generated file always starts with a package declaration using the configured generatedPackageName. This is required — the build fails if no package name is set.
Caching
The generation task is annotated with @CacheableTask. Gradle will skip re-generation when inputs (kenv directory contents) haven’t changed, making incremental builds fast.