Skip to content

Declaring Flags

kirich1409 edited this page May 18, 2026 · 2 revisions

Declaring Flags

Flags are declared in the module's build.gradle.kts using the featured { } DSL block. The Gradle plugin reads the declarations and generates typed Kotlin code — no runtime reflection, no string keys in application code.

DSL structure

// build.gradle.kts
featured {
    localFlags {
        boolean("new_checkout", default = false) {
            description = "Enable the new checkout flow"
            category = "Checkout"
        }
        int("max_cart_items", default = 10) {
            description = "Maximum items allowed in cart"
        }
    }
    remoteFlags {
        boolean("promo_banner", default = false) {
            description = "Show promo banner (remote-controlled)"
        }
    }
}

localFlags declares flags whose values come from a local provider (DataStore, SharedPreferences, etc.) or fall back to the declared default. remoteFlags declares flags fetched from a remote source (e.g., Firebase Remote Config).

Supported types

DSL function Kotlin type Generated accessor
boolean(key, default = …) Boolean fun ConfigValues.isXxxEnabled(): Boolean (for boolean), or fun ConfigValues.getXxx(): ConfigValue<Boolean>
int(key, default = …) Int fun ConfigValues.getXxx(): Int
enum(key, typeFqn, default) user-defined Enum<T> fun ConfigValues.getXxx(): T

Other types may be available — check the plugin's DSL for the full list. The documented types above are stable.

Enum flags — runtime requirement

Declaring an enum flag generates a typed ConfigParam<E>, but at runtime every storage-backed local provider must have an enumConverter<E>() registered via registerConverter(...) before the first read or write. Otherwise the provider throws IllegalArgumentException synchronously.

This applies to:

  • DataStoreConfigValueProvider
  • JavaPreferencesConfigValueProvider
  • SharedPreferencesProviderConfig

FirebaseConfigValueProvider handles enums automatically via reflection — no registration needed.

NSUserDefaultsConfigValueProvider does not support enums at this time — it has no converter API. See Known Limitations for the iOS workaround.

Example:

// Gradle DSL — declaration
featured {
    localFlags {
        enum(
            key = "checkout_variant",
            typeFqn = "com.example.CheckoutVariant",
            default = "LEGACY",
        )
    }
}
// Runtime — required wiring for non-Firebase local providers
val provider = DataStoreConfigValueProvider(dataStore).apply {
    registerConverter(enumConverter<CheckoutVariant>())
}
val configValues = ConfigValues(localProvider = provider)

Optional metadata fields

Inside the flag block, the following fields are optional:

  • description — human-readable description, shown in the debug UI
  • category — groups related flags in the debug UI
  • expiresAt — ISO date string; used by lint/Detekt rules to flag stale declarations

What the plugin generates

For a boolean flag named new_checkout, the plugin generates:

// Generated — do not edit
internal object GeneratedLocalFlags {
    val newCheckout: ConfigParam<Boolean> = ConfigParam(
        key = "new_checkout",
        defaultValue = false,
    )
}

// Public extension on ConfigValues
fun ConfigValues.isNewCheckoutEnabled(): Boolean =
    getValue(GeneratedLocalFlags.newCheckout).value

For an int flag named max_cart_items:

internal object GeneratedLocalFlags {
    val maxCartItems: ConfigParam<Int> = ConfigParam(
        key = "max_cart_items",
        defaultValue = 10,
    )
}

fun ConfigValues.getMaxCartItems(): Int =
    getValue(GeneratedLocalFlags.maxCartItems).value

Remote flags generate getXxx(): ConfigValue<T> extensions that expose both the value and its source (DEFAULT, LOCAL, or REMOTE).

Key naming convention

Flag keys use snake_case in the DSL. The plugin converts them to camelCase for the generated Kotlin identifiers. The raw key string is used as-is when reading from providers and when generating xcconfig conditions for iOS.

remoteFlags vs localFlags

localFlags declarations are eligible for dead-code elimination in release builds — the plugin generates R8 -assumevalues rules for every boolean local flag with default = false. remoteFlags values are dynamic (fetched at runtime) and are never eligible for DCE. See Release Optimization for details.

Clone this wiki locally