Skip to content

Sample App

kirich1409 edited this page Jun 13, 2026 · 2 revisions

Sample App

The sample is a multi-module KMP application demonstrating the full Featured plugin workflow across three independent feature modules, a shared aggregator, and three thin platform shells.

Module map

Module Role Flags
:sample:feature-checkout Feature module new_checkout (local Boolean, default false), checkout_variant (local enum CheckoutVariant, default LEGACY)
:sample:feature-promotions Feature module promo_banner_enabled (remote Boolean, default false)
:sample:feature-ui Feature module main_button_red (local Boolean, default true), new_feature_section_enabled (local Boolean, default true)
:sample:shared Aggregator + UI Applies dev.androidbroadcast.featured.application; no flag declarations of its own. Contains Compose UI (FeaturedSample, SampleApp) and SampleViewModel.
:sample:android-app Android shell Activity; wires DataStoreConfigValueProvider as the local provider and mounts FeatureFlagsDebugScreen.
:sample:desktop JVM shell main() entry point; uses InMemoryConfigValueProvider.
iosApp/ iOS shell Xcode project consuming FeaturedSampleApp.framework (static, produced by :sample:shared).

Observe-bridge pattern

Each :sample:feature-* module ships *FlagObservers.kt — public ConfigValues extension functions that expose the module's flags as Flows and suspend setters. UI consumers call these instead of reaching into GeneratedLocalFlags* / GeneratedRemoteFlags* directly.

Example from :sample:feature-checkout (CheckoutFlagObservers.kt):

public fun ConfigValues.newCheckoutFlow(): Flow<Boolean> =
    observe(GeneratedLocalFlagsSampleFeatureCheckout.newCheckout).map { it.value }

public fun ConfigValues.checkoutVariantFlow(): Flow<CheckoutVariant> =
    observe(GeneratedLocalFlagsSampleFeatureCheckout.checkoutVariant).map { it.value }

public suspend fun ConfigValues.setNewCheckout(value: Boolean) {
    override(GeneratedLocalFlagsSampleFeatureCheckout.newCheckout, value)
}

SampleViewModel in :sample:shared then consumes these extensions:

val newCheckout: StateFlow<Boolean> =
    configValues.newCheckoutFlow()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), false)

Aggregation in :sample:shared

:sample:shared declares featuredAggregation dependencies for all three feature modules and wires the generated output directory into commonMain:

// sample/shared/build.gradle.kts
plugins {
    id("dev.androidbroadcast.featured.application")
}

dependencies {
    featuredAggregation(project(":sample:feature-checkout"))
    featuredAggregation(project(":sample:feature-promotions"))
    featuredAggregation(project(":sample:feature-ui"))
}

sourceSets.commonMain.get().kotlin.srcDir(
    tasks.named("generateFeaturedRegistry").map { it.outputs.files.singleFile.parentFile },
)

The plugin generates object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> } which is passed to FeatureFlagsDebugScreen:

FeatureFlagsDebugScreen(
    configValues = configValues,
    registry = GeneratedFeaturedRegistry.all,
)

How to add a flag

  1. Edit the relevant feature module's build.gradle.kts — add a declaration inside featured { localFlags { ... } } or featured { remoteFlags { ... } }.
  2. Add a public observer / setter in *FlagObservers.kt referencing the generated GeneratedLocalFlags* or GeneratedRemoteFlags* object.
  3. If the UI needs it, expose a StateFlow + action in SampleViewModel.

For switching providers (DataStore, SharedPreferences, Firebase Remote Config), see Providers.

Clone this wiki locally