Featured is a type-safe, reactive feature-flag and configuration management library for Kotlin Multiplatform — Android, iOS (via SKIE), and JVM.
- Type-safe flags — declared in the Gradle DSL, accessed via generated typed extensions on
ConfigValues. No string keys, no unchecked casts. - Dead-code elimination in release builds — a flag with
default = falsemakes the guarded code unreachable. The Gradle plugin emits R8-assumevaluesrules (Android/JVM) and an xcconfig withDISABLE_<FLAG>Swift compilation conditions (iOS), so the respective compilers physically strip disabled branches from release binaries. - Reactive — every value is observable via
Flow; Compose and SwiftUI/Combine integrations included. - Multiple providers — DataStore, SharedPreferences, NSUserDefaults, JavaPreferences, Firebase Remote Config, ConfigCat, or a custom one.
- Debug UI — a ready-made Compose screen for overriding flags at runtime.
| 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:
- Getting Started
- Installation
- Providers
- Release Optimization (DCE) — how flags get stripped from release binaries
- iOS Usage
- Best Practices
See CONTRIBUTING.md.
See SECURITY.md.
MIT — see LICENSE.