Skip to content

Multi Module Setup

kirich1409 edited this page May 18, 2026 · 1 revision

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.

Apply the plugin per module

// :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"
        }
    }
}

Root aggregator task

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 generateXcconfig

Single ConfigValues in the app module

Feature 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)
}

One FlagRegistry per module (debug builds)

Each feature module registers its own flags with FlagRegistry in a debug-only block. The app module calls all of them before showing the debug UI:

// :feature:checkout/src/debug/kotlin/CheckoutDebugInitializer.kt
fun registerCheckoutFlags() {
    FlagRegistry.register(GeneratedLocalFlags.checkoutNewFlow)
}
// :app — Application.onCreate
if (BuildConfig.DEBUG) {
    registerCheckoutFlags()
    registerProfileFlags()
    // … other modules
}

Known limitation: Isolated projects

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.

Clone this wiki locally