Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

# 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

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

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

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

TypeKotlin TypeExample Value
StringString"hello"
IntInt42
LongLong123456789
DoubleDouble3.14
FloatFloat2.5
BooleanBooleantrue / false
UrlString"https://example.com"

Scope

ScopeMeaningValue Source
environmentDifferent per environmentenv.<name>.<ext> files
globalSame across all environmentsenv.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

ExtensionFormat
.envDotenv (KEY=VALUE)
.yaml / .ymlYAML mapping
.tomlTOML

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

  1. All environment-scoped variables must have a value in every env file
  2. All global-scoped variables must have a value in the global env file
  3. Type checking — values must parse as their declared type
  4. No defaults — the default field is not supported in v2; all values must be explicit
  5. 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.yaml and 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

ModeOutput 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

ConditionError
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

  1. You declare which build type maps to which environment
  2. The plugin generates a flat EnvConfig object per variant
  3. Each variant’s generated code is wired into its source set
  4. 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

ApproachBest For
Variant mappingAndroid-only projects where build types align with environments
Runtime selectionKMP projects, dynamic environment switching, server-driven config
Direct accessWhen 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:

  1. setActiveEnvironment(name) — Sets which environment’s values are returned by flat accessors
  2. Flat property accessors — Environment-scoped properties that delegate to the active environment
  3. 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

FeatureRuntime SelectionVariant Mapping
PlatformAll (KMP)Android only
Resolution timeRuntimeCompile time
SwitchingDynamicFixed per build
Code sizeAll environments includedOnly one environment
Use caseKMP, dynamic configAndroid 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:

  1. Runtime selection APIsetActiveEnvironment() and flat accessors
  2. Global properties — Top-level vals for global-scoped variables
  3. Per-environment nested objectsEnvConfig.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:

CharacterEscaped To
*/&#42;/
@&#64;

Type Mapping

Schema TypeKotlin TypeGenerated Literal
StringString"value"
IntInt42
LongLong123456789L
DoubleDouble3.14
FloatFloat2.5f
BooleanBooleantrue / false
UrlString"https://..."

String Escaping

Special characters in string values are properly escaped:

CharacterEscaped 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.