Skip to content

Declaring Flags

kirich1409 edited this page Jun 13, 2026 · 3 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 supports enums via registerConverter (see Providers).

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

Customizing generated code

The generation { } DSL block lets you override the package name, object name, and visibility of the generated sources. It can be declared module-wide in featured { generation { } } and overridden per section in localFlags { generation { } } / remoteFlags { generation { } }.

// build.gradle.kts
featured {
    generation {
        // Module-wide defaults (all optional)
        packageName = "com.example.checkout.flags" // default: dev.androidbroadcast.featured.generated
        visibility = FeaturedVisibility.INTERNAL   // default: INTERNAL
    }
    localFlags {
        generation {
            // Per-section overrides; fall back to the module-wide block above
            className = "CheckoutLocalFlags"       // default: GeneratedLocalFlags<ModuleSuffix>
            packageName = "com.example.checkout.local"
            visibility = FeaturedVisibility.PUBLIC
        }
        boolean("dark_mode", default = false) { category = "UI" }
    }
    remoteFlags {
        boolean("promo_banner", default = false)
    }
}

Properties:

Property Where Default Notes
packageName module-wide or per-section dev.androidbroadcast.featured.generated Affects flag objects, ConfigValues extensions, and iOS const-val files
className per-section only GeneratedLocalFlags<ModuleSuffix> / GeneratedRemoteFlags<ModuleSuffix> Replaces the entire default name; rejected in the top-level block (two objects, one name would collide)
visibility module-wide or per-section FeaturedVisibility.INTERNAL Applied to the generated object and its ConfigValues extensions; iOS const val declarations always stay public

FeaturedVisibility values: PUBLIC, INTERNAL.

ProGuard -assumevalues rules and iOS const-val files follow the local section's effective package, so dead-code elimination continues to work correctly with custom packages.

Expired-flag enforcement

Flags can carry an expiresAt date (ISO yyyy-MM-dd string). By default the build emits a warning for each flag whose expiry date is in the past. Set expiredFlagsMode = ExpiredFlagsMode.ERROR to turn those warnings into a build failure:

// build.gradle.kts
featured {
    expiredFlagsMode = ExpiredFlagsMode.ERROR

    localFlags {
        boolean("new_checkout", default = false) {
            expiresAt = "2025-12-31"
        }
    }
}

ExpiredFlagsMode values:

  • WARN (default) — a Gradle warning is emitted per expired flag; the build succeeds.
  • ERROR — the build fails with a GradleException that lists every expired flag's key, date, and module. Flags with an invalid expiresAt format are always reported as warnings even in ERROR mode.

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