-
Notifications
You must be signed in to change notification settings - Fork 0
Multi Module Setup
In a multi-module project, apply the Gradle plugin to every module that declares flags in featured { }. Each module manages its own flag declarations; the app module owns the single shared ConfigValues instance.
// :feature:checkout/build.gradle.kts
plugins {
id("dev.androidbroadcast.featured")
// … other plugins
}
featured {
localFlags {
boolean("checkout_new_flow", default = false) {
description = "Enable the redesigned checkout flow"
category = "checkout"
expiresAt = "2026-09-01"
}
}
}// :feature:profile/build.gradle.kts
plugins {
id("dev.androidbroadcast.featured")
}
featured {
localFlags {
boolean("profile_v2", default = false) {
description = "Enable the new profile screen"
category = "profile"
}
}
}The plugin registers a resolveFeatureFlags task per module and an aggregator task scanAllLocalFlags at the root project that collects flags across all modules.
# Resolve and aggregate flags across all modules
./gradlew scanAllLocalFlags
# Generate R8 rules for all Android modules
./gradlew generateFeaturedProguardRules
# Generate xcconfig across all modules
./gradlew generateXcconfigFeature modules declare their own ConfigParam objects but do not create ConfigValues. A single ConfigValues instance lives in the app module and is injected into feature modules through dependency injection:
// :app/src/main/kotlin/AppModule.kt (example with manual DI)
val configValues = ConfigValues(
localProvider = DataStoreConfigValueProvider(dataStore),
)
// Inject into :feature:checkout
val checkoutViewModel = CheckoutViewModel(configValues)Feature modules read flags through the generated extension functions on ConfigValues:
// In :feature:checkout
class CheckoutViewModel(private val configValues: ConfigValues) : ViewModel() {
val isNewFlowEnabled: StateFlow<Boolean> =
configValues.observe(GeneratedLocalFlags.checkoutNewFlow)
.map { it.value }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
}Apply the dev.androidbroadcast.featured.application plugin in the app module to aggregate flags from all feature modules into a single GeneratedFeaturedRegistry.all list:
// :app/build.gradle.kts
plugins {
id("dev.androidbroadcast.featured")
id("dev.androidbroadcast.featured.application")
}
dependencies {
featuredAggregation(project(":feature-checkout"))
featuredAggregation(project(":feature-profile"))
// For modules with enum flags, also a regular implementation dep
// so the enum class is on the compile classpath:
implementation(project(":feature-checkout"))
}
// Wire the generated source set:
kotlin.sourceSets.commonMain.kotlin.srcDir(
layout.buildDirectory.dir("generated/featured/commonMain"),
)The plugin reads the featured-manifest.json artifact published by each featuredAggregation dependency and generates:
object GeneratedFeaturedRegistry {
val all: List<ConfigParam<*>> = listOf(/* all aggregated flags */)
}Pass this list to the debug UI:
FeatureFlagsDebugScreen(
configValues = configValues,
registry = GeneratedFeaturedRegistry.all,
)Primitive-only modules need only a featuredAggregation(...) dependency. Modules that declare enum flags also require a regular implementation(project(...)) so the enum class is available on the compile classpath at the app level.
The plugin is Configuration Cache safe but not isolated-projects safe. The wireToRootAggregator() call in FeaturedPlugin.kt accesses target.rootProject to register the scanAllLocalFlags aggregator, which violates the isolated-projects contract. This is intentional for 1.0.0-Beta and will be addressed in v1.1.0 by converting the aggregator wiring to a settings plugin. See Known Limitations for the tracking issue.