
Type-safe, reactive feature-flag and configuration manager with generated typed helpers, runtime local/remote providers, Compose-friendly observers, debug UI, and build-time dead-code elimination for disabled flags.
Featured is a type-safe, reactive feature-flag and configuration management library for Kotlin Multiplatform — Android, iOS (via SKIE), and JVM.
ConfigValues. No string keys, no unchecked casts.default = false makes the guarded code unreachable. The Gradle plugin emits R8 -assumevalues rules (Android/JVM) and an xcconfig with DISABLE_<FLAG> Swift compilation conditions (iOS), so the respective compilers physically strip disabled branches from release binaries.Flow; Compose and SwiftUI/Combine integrations included.| Platform | Status |
|---|---|
| Android | Stable |
| iOS (SKIE / DCE) | Preview |
| JVM | Preview |
Preview means the platform is functional but its public API may change in minor releases without a major version bump. Stable platforms follow Semantic Versioning.
// build.gradle.kts — declare the flag
plugins {
id("dev.androidbroadcast.featured") version "<version>"
}
dependencies {
implementation(platform("dev.androidbroadcast.featured:featured-bom:<version>"))
implementation("dev.androidbroadcast.featured:featured-core")
implementation("dev.androidbroadcast.featured:featured-datastore-provider")
}
featured {
localFlags {
boolean("new_checkout", default = false) {
description = "Enable the new checkout flow"
}
}
}// Application.kt — wire up ConfigValues once
val dataStore = PreferenceDataStoreFactory.create { context.dataStoreFile("feature_flags.preferences_pb") }
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
)// Read the generated extension anywhere
val isEnabled: Boolean = configValues.isNewCheckoutEnabled()By default the plugin generates internal objects named GeneratedLocalFlags<ModuleSuffix> /
GeneratedRemoteFlags<ModuleSuffix> in the dev.androidbroadcast.featured.generated package.
The generation { } block overrides the package, class names, and visibility — module-wide in
featured { } and per section inside localFlags { } / remoteFlags { } (section values win):
import dev.androidbroadcast.featured.gradle.FeaturedVisibility
featured {
generation { // module-wide defaults
packageName = "com.example.checkout.flags"
visibility = FeaturedVisibility.INTERNAL
}
localFlags {
generation { // overrides for local flags only
className = "CheckoutLocalFlags" // exact name, no module suffix appended
visibility = FeaturedVisibility.PUBLIC
}
boolean("new_checkout", default = false)
}
}The generated .kt file is named after the custom class name. With a custom name the
module-suffix-based JVM-name uniqueness no longer applies — make sure two modules don't
generate the same package + class name. ProGuard/R8 -assumevalues rules and the iOS
const-val files automatically follow the local section's effective package, so release-build
dead-code elimination keeps working with custom packages.
In a multi-module app, construct one ConfigValues per feature module plus one debug aggregator,
all sharing the same LocalConfigValueProvider:
// Construct one ConfigValues per feature module + one debug aggregator, all over a shared provider
val sharedLocal: LocalConfigValueProvider = defaultLocalProvider(applicationContext)
val checkoutConfig = ConfigValues(localProvider = sharedLocal)
val promotionsConfig = ConfigValues(localProvider = sharedLocal)
val uiConfig = ConfigValues(localProvider = sharedLocal)
// Debug-only aggregator that the FeatureFlagsDebugScreen drives
val debugConfig = ConfigValues(localProvider = sharedLocal)
FeatureFlagsDebugScreen(
configValues = debugConfig,
registry = GeneratedFeaturedRegistry.all,
)Each feature module owns its own ConfigValues and observes only its own flags (via public
observe-bridge extensions). The generated GeneratedLocalFlagsX / GeneratedRemoteFlagsX objects
are internal to their module — cross-module flag listing flows exclusively through
GeneratedFeaturedRegistry.all, which is built from the per-module manifests by the aggregator
plugin. The single source of truth for stored overrides is the shared LocalConfigValueProvider,
so writes from any instance propagate to every other one through its reactive observe flow.
Full documentation lives in the Wiki:
See CONTRIBUTING.md.
See SECURITY.md.
MIT — see LICENSE.
Featured is a type-safe, reactive feature-flag and configuration management library for Kotlin Multiplatform — Android, iOS (via SKIE), and JVM.
ConfigValues. No string keys, no unchecked casts.default = false makes the guarded code unreachable. The Gradle plugin emits R8 -assumevalues rules (Android/JVM) and an xcconfig with DISABLE_<FLAG> Swift compilation conditions (iOS), so the respective compilers physically strip disabled branches from release binaries.Flow; Compose and SwiftUI/Combine integrations included.| Platform | Status |
|---|---|
| Android | Stable |
| iOS (SKIE / DCE) | Preview |
| JVM | Preview |
Preview means the platform is functional but its public API may change in minor releases without a major version bump. Stable platforms follow Semantic Versioning.
// build.gradle.kts — declare the flag
plugins {
id("dev.androidbroadcast.featured") version "<version>"
}
dependencies {
implementation(platform("dev.androidbroadcast.featured:featured-bom:<version>"))
implementation("dev.androidbroadcast.featured:featured-core")
implementation("dev.androidbroadcast.featured:featured-datastore-provider")
}
featured {
localFlags {
boolean("new_checkout", default = false) {
description = "Enable the new checkout flow"
}
}
}// Application.kt — wire up ConfigValues once
val dataStore = PreferenceDataStoreFactory.create { context.dataStoreFile("feature_flags.preferences_pb") }
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
)// Read the generated extension anywhere
val isEnabled: Boolean = configValues.isNewCheckoutEnabled()By default the plugin generates internal objects named GeneratedLocalFlags<ModuleSuffix> /
GeneratedRemoteFlags<ModuleSuffix> in the dev.androidbroadcast.featured.generated package.
The generation { } block overrides the package, class names, and visibility — module-wide in
featured { } and per section inside localFlags { } / remoteFlags { } (section values win):
import dev.androidbroadcast.featured.gradle.FeaturedVisibility
featured {
generation { // module-wide defaults
packageName = "com.example.checkout.flags"
visibility = FeaturedVisibility.INTERNAL
}
localFlags {
generation { // overrides for local flags only
className = "CheckoutLocalFlags" // exact name, no module suffix appended
visibility = FeaturedVisibility.PUBLIC
}
boolean("new_checkout", default = false)
}
}The generated .kt file is named after the custom class name. With a custom name the
module-suffix-based JVM-name uniqueness no longer applies — make sure two modules don't
generate the same package + class name. ProGuard/R8 -assumevalues rules and the iOS
const-val files automatically follow the local section's effective package, so release-build
dead-code elimination keeps working with custom packages.
In a multi-module app, construct one ConfigValues per feature module plus one debug aggregator,
all sharing the same LocalConfigValueProvider:
// Construct one ConfigValues per feature module + one debug aggregator, all over a shared provider
val sharedLocal: LocalConfigValueProvider = defaultLocalProvider(applicationContext)
val checkoutConfig = ConfigValues(localProvider = sharedLocal)
val promotionsConfig = ConfigValues(localProvider = sharedLocal)
val uiConfig = ConfigValues(localProvider = sharedLocal)
// Debug-only aggregator that the FeatureFlagsDebugScreen drives
val debugConfig = ConfigValues(localProvider = sharedLocal)
FeatureFlagsDebugScreen(
configValues = debugConfig,
registry = GeneratedFeaturedRegistry.all,
)Each feature module owns its own ConfigValues and observes only its own flags (via public
observe-bridge extensions). The generated GeneratedLocalFlagsX / GeneratedRemoteFlagsX objects
are internal to their module — cross-module flag listing flows exclusively through
GeneratedFeaturedRegistry.all, which is built from the per-module manifests by the aggregator
plugin. The single source of truth for stored overrides is the shared LocalConfigValueProvider,
so writes from any instance propagate to every other one through its reactive observe flow.
Full documentation lives in the Wiki:
See CONTRIBUTING.md.
See SECURITY.md.
MIT — see LICENSE.