Featured
1.2.0indexedType-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.
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.
| Platform | Status |
|---|---|
| Android | Stable |
| iOS (SKIE / DCE) | Preview |
| JVM |
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
and per section inside / (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 ,
so writes from any instance propagate to every other one through its reactive flow.
Full documentation lives in the Wiki:
See CONTRIBUTING.md.
See SECURITY.md.
MIT — see LICENSE.
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.| Preview |
featured { }localFlags { }remoteFlags { }LocalConfigValueProviderobserveSurfaced from shared tags and platforms — no rankings paid for.