diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 328bb40..97d2b8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ jobs: :providers:sharedpreferences:koverVerify :providers:firebase:koverVerify :featured-compose:koverVerify - :featured-registry:koverVerify :featured-testing:koverVerify - uses: actions/upload-artifact@v7 if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe9f6c..7fc14e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- `featured-registry` module — the runtime `FlagRegistry` global singleton and its `FlagRegistryDelegate` expect/actual are removed. Use `GeneratedFeaturedRegistry.all` (produced by the `dev.androidbroadcast.featured.application` plugin) or build an explicit `List>` instead. +- `featured-gradle-plugin` — `generateFlagRegistrar` task, `FlagRegistrarGenerator`, and `GenerateFlagRegistrarTask` are removed. Per-module `GeneratedFlagRegistrar.kt` files are no longer generated. +- Sample API — `registerSampleFlags()` is removed (was specific to the sample app's legacy wiring). The sample now uses `SampleFeatureFlags.all` directly. + +### Changed + +- `FeatureFlagsDebugScreen` signature is now `(configValues: ConfigValues, registry: List>, modifier: Modifier = Modifier)` — accepts an explicit registry list instead of reading the (removed) `FlagRegistry` singleton. Pass `GeneratedFeaturedRegistry.all` for the recommended aggregator-plugin flow, or build the list inline for small projects. + ### Added - Featured library plugin now publishes a per-module feature-flag manifest as a consumable Gradle artifact (`featuredManifest` configuration, schema v1). Existing flag-generation pipeline is unchanged. Consumer-side aggregation arrives in a follow-up release. diff --git a/featured-bom/build.gradle.kts b/featured-bom/build.gradle.kts index 9bd2fff..d00b7cf 100644 --- a/featured-bom/build.gradle.kts +++ b/featured-bom/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { api(project(":providers:configcat")) api(project(":featured-compose")) - api(project(":featured-registry")) api(project(":featured-debug-ui")) api(project(":featured-testing")) api(project(":featured-gradle-plugin")) diff --git a/featured-debug-ui/README.md b/featured-debug-ui/README.md index bc94558..6c59145 100644 --- a/featured-debug-ui/README.md +++ b/featured-debug-ui/README.md @@ -6,18 +6,28 @@ Ready-made Compose screen for runtime flag inspection and override. Intended for ```kotlin debugImplementation("dev.androidbroadcast.featured:featured-debug-ui") -debugImplementation("dev.androidbroadcast.featured:featured-registry") // required ``` ## Usage -Embed `FeatureFlagsDebugScreen` behind a dev-menu, shake gesture, or debug settings screen: +Embed `FeatureFlagsDebugScreen` behind a dev-menu, shake gesture, or debug settings screen. Pass an explicit `List>` as the registry. + +**Recommended — use the aggregator plugin** (multi-module projects): apply `dev.androidbroadcast.featured.application` in the app module and declare feature modules via `featuredAggregation(project(...))`. The plugin generates `GeneratedFeaturedRegistry.all` at build time. + +```kotlin +FeatureFlagsDebugScreen( + configValues = configValues, + registry = GeneratedFeaturedRegistry.all, +) +``` + +**Alternative — inline list** (small / single-module projects): ```kotlin FeatureFlagsDebugScreen( - registry = FlagRegistry.instance, configValues = configValues, + registry = listOf(MyFlags.flagA, MyFlags.flagB), ) ``` -The screen displays all registered flags grouped by category, shows the current value and its source (DEFAULT / LOCAL / REMOTE), and allows overriding any flag value in-process. Overrides are applied via `ConfigValues.override()` and survive until the app process is restarted (or `clearOverrides()` is called). +The screen displays all registry flags grouped by category, shows the current value and its source (DEFAULT / LOCAL / REMOTE), and allows overriding any flag value in-process. Overrides are applied via `ConfigValues.override()` and survive until the app process is restarted (or `clearOverrides()` is called). diff --git a/featured-debug-ui/build.gradle.kts b/featured-debug-ui/build.gradle.kts index 3ca03f4..7b2883f 100644 --- a/featured-debug-ui/build.gradle.kts +++ b/featured-debug-ui/build.gradle.kts @@ -44,7 +44,6 @@ kotlin { sourceSets { commonMain.dependencies { implementation(projects.core) - implementation(projects.featuredRegistry) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) diff --git a/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt b/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt index 6d5361d..0e4828b 100644 --- a/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt +++ b/featured-debug-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/debugui/FeatureFlagsDebugScreen.kt @@ -38,12 +38,11 @@ import androidx.compose.ui.unit.dp import dev.androidbroadcast.featured.ConfigParam import dev.androidbroadcast.featured.ConfigValue import dev.androidbroadcast.featured.ConfigValues -import dev.androidbroadcast.featured.registry.FlagRegistry import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** - * A ready-to-use debug screen that lists all feature flags registered in [FlagRegistry] + * A ready-to-use debug screen that lists all feature flags in the provided [registry] * and allows toggling boolean flags or viewing current values for other types. * * Flags are grouped by [ConfigParam.category]. Each flag shows its current value, source @@ -55,7 +54,17 @@ import kotlinx.coroutines.launch * * Intended for debug/internal builds only. * + * Pass `GeneratedFeaturedRegistry.all` (from the `dev.androidbroadcast.featured.application` + * plugin) or build the list explicitly. + * * @param configValues The [ConfigValues] instance used to read and override flag values. + * @param registry The list of [ConfigParam] instances to display. Must be a stable + * reference (a top-level `val`, an `object` property, or a `remember`-ed list). + * The screen keys its internal `LaunchedEffect` on this list via `equals` (structural). + * A freshly-allocated list on every recomposition may restart the effect; prefer a + * stable top-level `val` or `object` property for predictable behavior. + * Each [ConfigParam.key] must be unique within the list; duplicates cause a + * runtime crash in `LazyColumn` key collision. * @param modifier Optional [Modifier] for the root composable. */ @OptIn(ExperimentalMaterial3Api::class) @@ -63,6 +72,7 @@ import kotlinx.coroutines.launch @Suppress("ktlint:standard:function-naming") public fun FeatureFlagsDebugScreen( configValues: ConfigValues, + registry: List>, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() @@ -70,16 +80,15 @@ public fun FeatureFlagsDebugScreen( mutableStateOf>>>(emptyMap()) } - LaunchedEffect(configValues) { - val params = FlagRegistry.all() - groupedItems = groupFlagsByCategory(buildDebugItems(configValues, params)) + LaunchedEffect(configValues, registry) { + groupedItems = groupFlagsByCategory(buildDebugItems(configValues, registry)) // Reactive: observe all params and refresh on any change. // On each emission all params are re-read — acceptable for a debug-only screen. - val flows = params.map { param -> configValues.observe(param) } + val flows = registry.map { param -> configValues.observe(param) } if (flows.isNotEmpty()) { flows.merge().collect { - groupedItems = groupFlagsByCategory(buildDebugItems(configValues, params)) + groupedItems = groupFlagsByCategory(buildDebugItems(configValues, registry)) } } } @@ -97,7 +106,7 @@ public fun FeatureFlagsDebugScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "No feature flags registered.", + text = "No feature flags to display.", style = MaterialTheme.typography.bodyMedium, ) } diff --git a/featured-gradle-plugin/CLAUDE.md b/featured-gradle-plugin/CLAUDE.md index 5f50182..df44b59 100644 --- a/featured-gradle-plugin/CLAUDE.md +++ b/featured-gradle-plugin/CLAUDE.md @@ -27,7 +27,6 @@ featured { |------|--------| | `resolveFeatureFlags` | `build/featured/flags.txt` | | `generateConfigParam` | `build/generated/featured/commonMain/Generated{Local,Remote}Flags.kt` + `GeneratedFlagExtensions.kt` | -| `generateFlagRegistrar` | `build/generated/featured/GeneratedFlagRegistrar.kt` | | `generateFeaturedProguardRules` | `build/featured/proguard-featured.pro` | | `generateIosConstVal` | iOS constant value files | | `generateXcconfig` | `build/featured/FeatureFlags.generated.xcconfig` | diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt index 55bd264..b331e8b 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt @@ -13,7 +13,6 @@ import org.gradle.api.tasks.TaskProvider internal const val RESOLVE_FLAGS_TASK_NAME = "resolveFeatureFlags" internal const val SCAN_ALL_TASK_NAME = "scanAllLocalFlags" -internal const val GENERATE_FLAG_REGISTRAR_TASK_NAME = "generateFlagRegistrar" internal const val GENERATE_PROGUARD_TASK_NAME = "generateFeaturedProguardRules" internal const val GENERATE_IOS_CONST_VAL_TASK_NAME = "generateIosConstVal" internal const val GENERATE_XCCONFIG_TASK_NAME = "generateXcconfig" @@ -24,8 +23,7 @@ internal const val GENERATE_CONFIG_PARAM_TASK_NAME = "generateConfigParam" * 1. Exposes the `featured { }` DSL extension for declaring local and remote feature flags. * 2. Generates typed `ConfigParam` objects and ergonomic `ConfigValues` extension functions. * 3. Generates per-function R8 `-assumevalues` rules for local flags (dead-code elimination). - * 4. Generates a `GeneratedFlagRegistrar` that registers all flags with `FlagRegistry`. - * 5. Generates iOS constant-value files and xcconfig for Swift dead-code elimination. + * 4. Generates iOS constant-value files and xcconfig for Swift dead-code elimination. * * Usage in `build.gradle.kts`: * ```kotlin @@ -57,7 +55,6 @@ public class FeaturedPlugin : Plugin { } registerConfigParamTask(target, resolveTask) - registerFlagRegistrarTask(target, resolveTask) val proguardTask = registerProguardTask(target, resolveTask) registerIosConstValTask(target, resolveTask) registerXcconfigTask(target, resolveTask) @@ -94,22 +91,6 @@ public class FeaturedPlugin : Plugin { } } - private fun registerFlagRegistrarTask( - target: Project, - resolveTask: TaskProvider, - ) { - target.tasks.register(GENERATE_FLAG_REGISTRAR_TASK_NAME, GenerateFlagRegistrarTask::class.java) { task -> - task.group = "featured" - task.description = "Generates GeneratedFlagRegistrar.kt for '${target.path}'." - task.scanResultFile.set(resolveTask.flatMap { it.outputFile }) - task.packageName.set("dev.androidbroadcast.featured.generated") - task.outputFile.set( - target.layout.buildDirectory.file("generated/featured/GeneratedFlagRegistrar.kt"), - ) - task.dependsOn(resolveTask) - } - } - private fun registerProguardTask( target: Project, resolveTask: TaskProvider, diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGenerator.kt deleted file mode 100644 index 72c1296..0000000 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGenerator.kt +++ /dev/null @@ -1,65 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -/** - * Generates `GeneratedFlagRegistrar.kt` — an object with a `register()` function that - * calls `FlagRegistry.register(...)` for every flag declared in this module. - * - * The generated references point to the objects produced by [ConfigParamGenerator]: - * ```kotlin - * public object GeneratedFlagRegistrar { - * public fun register() { - * FlagRegistry.register(GeneratedLocalFlags.darkMode) - * FlagRegistry.register(GeneratedRemoteFlags.newCheckout) - * } - * } - * ``` - * - * This file is KMP-safe: it imports only `dev.androidbroadcast.featured.registry.FlagRegistry` - * which is available in `commonMain`. - */ -public object FlagRegistrarGenerator { - private const val FLAG_REGISTRY_IMPORT = "dev.androidbroadcast.featured.registry.FlagRegistry" - private const val LOCAL_FLAGS_IMPORT = - "dev.androidbroadcast.featured.generated.${LocalFlagEntry.GENERATED_LOCAL_OBJECT}" - private const val REMOTE_FLAGS_IMPORT = - "dev.androidbroadcast.featured.generated.${LocalFlagEntry.GENERATED_REMOTE_OBJECT}" - - /** - * Generates the `GeneratedFlagRegistrar.kt` source text. - * - * When [entries] is empty the `register()` body is empty but the file is still emitted - * so the compilation source set always contains a valid symbol. - * - * @param entries All flag entries for this module (local + remote). - * @param packageName Package for the generated file. - */ - public fun generate( - entries: List, - packageName: String = "dev.androidbroadcast.featured.generated", - ): String = - buildString { - appendLine("// Auto-generated by Featured Gradle Plugin — do not edit manually.") - appendLine("package $packageName") - appendLine() - appendLine("import $FLAG_REGISTRY_IMPORT") - if (entries.any { it.isLocal }) appendLine("import $LOCAL_FLAGS_IMPORT") - if (entries.any { !it.isLocal }) appendLine("import $REMOTE_FLAGS_IMPORT") - appendLine() - appendLine("public object GeneratedFlagRegistrar {") - appendLine(" /**") - appendLine(" * Registers all flags declared in this module with [FlagRegistry].") - appendLine(" * Call this once during app startup.") - appendLine(" */") - appendLine(" public fun register() {") - entries.forEach { entry -> - val ref = entry.kotlinReference - if (ref.isNotBlank()) { - appendLine(" FlagRegistry.register($ref)") - } else { - appendLine(" // TODO: register flag '${entry.key}' — property reference unavailable") - } - } - appendLine(" }") - append("}") - } -} diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTask.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTask.kt deleted file mode 100644 index 8917582..0000000 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTask.kt +++ /dev/null @@ -1,70 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -/** - * Gradle task that reads the [ScanLocalFlagsTask] output file and generates a - * `GeneratedFlagRegistrar.kt` source file containing an `object GeneratedFlagRegistrar` - * with a single `register()` function that calls `FlagRegistry.register(...)` for every - * `@LocalFlag`-annotated `ConfigParam` in this module. - * - * The generated file is KMP-safe — it uses only APIs available in `commonMain`. - * - * Wire the generated source directory into the Kotlin compilation manually: - * ```kotlin - * kotlin { - * sourceSets.commonMain.get().kotlin.srcDir( - * tasks.named("generateFlagRegistrar").map { it.outputFile.get().asFile.parentFile } - * ) - * } - * ``` - */ -@CacheableTask -public abstract class GenerateFlagRegistrarTask : DefaultTask() { - /** - * The line-delimited flag report produced by [ScanLocalFlagsTask]. - * Each line has the format `key|defaultValue|type|moduleName|propertyName|ownerName`. - */ - @get:InputFile - @get:PathSensitive(PathSensitivity.NONE) - public abstract val scanResultFile: RegularFileProperty - - /** - * Kotlin package name used in the generated `GeneratedFlagRegistrar` object. - * Defaults to `"dev.androidbroadcast.featured.generated"`. - */ - @get:Input - public abstract val packageName: Property - - /** - * The generated `GeneratedFlagRegistrar.kt` file. - * Written to `/build/generated/featured/GeneratedFlagRegistrar.kt`. - */ - @get:OutputFile - public abstract val outputFile: RegularFileProperty - - @TaskAction - public fun generate() { - val entries = scanResultFile.parseLocalFlagEntries() - val source = FlagRegistrarGenerator.generate(entries, packageName.get()) - - val out = outputFile.get().asFile - out.parentFile?.mkdirs() - out.writeText(source) - - if (entries.isEmpty()) { - logger.lifecycle("[featured] No flags declared in featured { } DSL — GeneratedFlagRegistrar.register() is empty.") - } else { - logger.lifecycle("[featured] Generated FlagRegistrar with ${entries.size} registration(s) → ${out.path}") - } - } -} diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt index 503b09a..a4b072c 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt @@ -38,21 +38,6 @@ public data class LocalFlagEntry( */ public val isEnum: Boolean get() = '.' in type - /** - * Returns the Kotlin reference used in the generated `FlagRegistry.register(...)` call. - * - * - Local flags: `"GeneratedLocalFlags.propertyName"` - * - Remote flags: `"GeneratedRemoteFlags.propertyName"` - * - Blank when [propertyName] is empty (legacy data without property information). - */ - public val kotlinReference: String - get() = - when { - propertyName.isBlank() -> "" - isLocal -> "$GENERATED_LOCAL_OBJECT.$propertyName" - else -> "$GENERATED_REMOTE_OBJECT.$propertyName" - } - public companion object { public const val FLAG_TYPE_LOCAL: String = "local" public const val FLAG_TYPE_REMOTE: String = "remote" diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt index 6f195f0..dbe2fae 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ResolveFlagsTask.kt @@ -18,8 +18,8 @@ import org.gradle.api.tasks.TaskAction * * where `propertyName` is the camelCase conversion of `key` (e.g. `dark_mode` → `darkMode`). * - * Downstream tasks ([GenerateFlagRegistrarTask], [GenerateProguardRulesTask], - * [GenerateConfigParamTask], etc.) declare [outputFile] as their `@InputFile` to + * Downstream tasks ([GenerateProguardRulesTask], [GenerateConfigParamTask], etc.) + * declare [outputFile] as their `@InputFile` to * establish a proper task dependency and enable configuration-cache compatibility. */ @CacheableTask diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGeneratorTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGeneratorTest.kt deleted file mode 100644 index 3cab987..0000000 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGeneratorTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -import kotlin.test.Test -import kotlin.test.assertContains -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class FlagRegistrarGeneratorTest { - @Test - fun `generates package declaration`() { - val source = FlagRegistrarGenerator.generate(emptyList(), packageName = "com.example.generated") - assertContains(source, "package com.example.generated") - } - - @Test - fun `generates FlagRegistry import`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertContains(source, "import dev.androidbroadcast.featured.registry.FlagRegistry") - } - - @Test - fun `generates public GeneratedFlagRegistrar object`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertContains(source, "public object GeneratedFlagRegistrar") - } - - @Test - fun `generates public register function`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertContains(source, "public fun register()") - } - - @Test - fun `generates empty register body when no entries`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertFalse(source.contains("FlagRegistry.register("), "No register calls for empty entries") - } - - @Test - fun `generates GeneratedLocalFlags import for local flag`() { - val entries = listOf(localEntry("dark_mode", "darkMode")) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "import dev.androidbroadcast.featured.generated.${LocalFlagEntry.GENERATED_LOCAL_OBJECT}") - } - - @Test - fun `generates GeneratedRemoteFlags import for remote flag`() { - val entries = listOf(remoteEntry("promo_banner", "promoBanner")) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "import dev.androidbroadcast.featured.generated.${LocalFlagEntry.GENERATED_REMOTE_OBJECT}") - } - - @Test - fun `does not generate local import when no local flags`() { - val entries = listOf(remoteEntry("promo", "promo")) - val source = FlagRegistrarGenerator.generate(entries) - assertFalse(source.contains(LocalFlagEntry.GENERATED_LOCAL_OBJECT + "\n")) - } - - @Test - fun `generates register call referencing GeneratedLocalFlags for local flag`() { - val entries = listOf(localEntry("dark_mode", "darkMode")) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "FlagRegistry.register(${LocalFlagEntry.GENERATED_LOCAL_OBJECT}.darkMode)") - } - - @Test - fun `generates register call referencing GeneratedRemoteFlags for remote flag`() { - val entries = listOf(remoteEntry("promo_banner", "promoBanner")) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "FlagRegistry.register(${LocalFlagEntry.GENERATED_REMOTE_OBJECT}.promoBanner)") - } - - @Test - fun `generates register calls for mixed local and remote entries`() { - val entries = - listOf( - localEntry("dark_mode", "darkMode"), - remoteEntry("promo_banner", "promoBanner"), - ) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "FlagRegistry.register(${LocalFlagEntry.GENERATED_LOCAL_OBJECT}.darkMode)") - assertContains(source, "FlagRegistry.register(${LocalFlagEntry.GENERATED_REMOTE_OBJECT}.promoBanner)") - } - - @Test - fun `generates TODO comment for blank property name`() { - val entries = - listOf( - LocalFlagEntry(key = "legacy_flag", defaultValue = "false", type = "Boolean", moduleName = ":app"), - ) - val source = FlagRegistrarGenerator.generate(entries) - assertContains(source, "// TODO: register flag 'legacy_flag'") - assertFalse(source.contains("FlagRegistry.register()")) - } - - @Test - fun `generated source contains auto-generated comment`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertContains(source, "Auto-generated by Featured Gradle Plugin") - } - - @Test - fun `uses default package name when not specified`() { - val source = FlagRegistrarGenerator.generate(emptyList()) - assertContains(source, "package dev.androidbroadcast.featured.generated") - } - - @Test - fun `generated source has balanced braces`() { - val entries = listOf(localEntry("flag", "flag")) - val source = FlagRegistrarGenerator.generate(entries) - val open = source.count { it == '{' } - val close = source.count { it == '}' } - assertTrue(open == close && open >= 2, "Expected balanced braces, open=$open close=$close") - } - - // ── helpers ─────────────────────────────────────────────────────────────── - - private fun localEntry( - key: String, - propertyName: String, - ) = LocalFlagEntry( - key = key, - defaultValue = "false", - type = "Boolean", - moduleName = ":app", - propertyName = propertyName, - flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, - ) - - private fun remoteEntry( - key: String, - propertyName: String, - ) = LocalFlagEntry( - key = key, - defaultValue = "false", - type = "Boolean", - moduleName = ":app", - propertyName = propertyName, - flagType = LocalFlagEntry.FLAG_TYPE_REMOTE, - ) -} diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTaskRegistrationTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTaskRegistrationTest.kt deleted file mode 100644 index 4993e8f..0000000 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTaskRegistrationTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -import org.gradle.testfixtures.ProjectBuilder -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class GenerateFlagRegistrarTaskRegistrationTest { - @Test - fun `plugin registers generateFlagRegistrar task`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - assertNotNull( - project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME), - "Expected '$GENERATE_FLAG_REGISTRAR_TASK_NAME' task to be registered by the plugin", - ) - } - - @Test - fun `generateFlagRegistrar task is of correct type`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val task = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) - assertNotNull(task) - assertTrue( - task is GenerateFlagRegistrarTask, - "Expected task type GenerateFlagRegistrarTask but was ${task::class.simpleName}", - ) - } - - @Test - fun `generateFlagRegistrar task is in featured group`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val task = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) - assertNotNull(task) - assertEquals( - "featured", - task.group, - "Expected task group 'featured' but was '${task.group}'", - ) - } - - @Test - fun `generateFlagRegistrar task has outputFile configured`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val task = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) as? GenerateFlagRegistrarTask - assertNotNull(task) - assertTrue( - task.outputFile.isPresent, - "Expected outputFile to be configured on GenerateFlagRegistrarTask", - ) - } - - @Test - fun `generateFlagRegistrar task has packageName configured`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val task = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) as? GenerateFlagRegistrarTask - assertNotNull(task) - assertTrue( - task.packageName.isPresent, - "Expected packageName to be configured on GenerateFlagRegistrarTask", - ) - assertEquals( - "dev.androidbroadcast.featured.generated", - task.packageName.get(), - "Expected default package name 'dev.androidbroadcast.featured.generated'", - ) - } - - @Test - fun `generateFlagRegistrar task depends on resolveFeatureFlags task`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val generateTask = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) - assertNotNull(generateTask) - val scanTask = project.tasks.findByName(RESOLVE_FLAGS_TASK_NAME) - assertNotNull(scanTask) - assertTrue( - generateTask.taskDependencies.getDependencies(generateTask).contains(scanTask), - "Expected '$GENERATE_FLAG_REGISTRAR_TASK_NAME' to depend on '$RESOLVE_FLAGS_TASK_NAME'", - ) - } - - @Test - fun `generateFlagRegistrar outputFile is inside build generated featured directory`() { - val project = ProjectBuilder.builder().build() - project.plugins.apply("dev.androidbroadcast.featured") - - val task = project.tasks.findByName(GENERATE_FLAG_REGISTRAR_TASK_NAME) as? GenerateFlagRegistrarTask - assertNotNull(task) - val outputPath = - task.outputFile - .get() - .asFile.path - assertTrue( - outputPath.contains("generated/featured"), - "Expected outputFile inside 'generated/featured' directory, got: $outputPath", - ) - } -} diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryKotlinReferenceTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryKotlinReferenceTest.kt deleted file mode 100644 index 49a6762..0000000 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryKotlinReferenceTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package dev.androidbroadcast.featured.gradle - -import kotlin.test.Test -import kotlin.test.assertEquals - -class LocalFlagEntryKotlinReferenceTest { - @Test - fun `kotlinReference for local flag uses GeneratedLocalFlags object`() { - val entry = - LocalFlagEntry( - key = "dark_mode", - defaultValue = "false", - type = "Boolean", - moduleName = ":app", - propertyName = "darkMode", - flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, - ) - assertEquals("${LocalFlagEntry.GENERATED_LOCAL_OBJECT}.darkMode", entry.kotlinReference) - } - - @Test - fun `kotlinReference for remote flag uses GeneratedRemoteFlags object`() { - val entry = - LocalFlagEntry( - key = "promo_banner", - defaultValue = "false", - type = "Boolean", - moduleName = ":app", - propertyName = "promoBanner", - flagType = LocalFlagEntry.FLAG_TYPE_REMOTE, - ) - assertEquals("${LocalFlagEntry.GENERATED_REMOTE_OBJECT}.promoBanner", entry.kotlinReference) - } - - @Test - fun `kotlinReference returns empty string when propertyName is blank`() { - val entry = - LocalFlagEntry( - key = "legacy", - defaultValue = "false", - type = "Boolean", - moduleName = ":app", - propertyName = "", - flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, - ) - assertEquals("", entry.kotlinReference) - } - - @Test - fun `kotlinReference returns empty string for default-constructed entry`() { - val entry = LocalFlagEntry(key = "k", defaultValue = "v", type = "String", moduleName = ":mod") - assertEquals("", entry.kotlinReference) - } - - @Test - fun `isLocal is true for local flagType`() { - val entry = - LocalFlagEntry( - key = "k", - defaultValue = "v", - type = "String", - moduleName = ":mod", - flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, - ) - assertEquals(true, entry.isLocal) - } - - @Test - fun `isLocal is false for remote flagType`() { - val entry = - LocalFlagEntry( - key = "k", - defaultValue = "v", - type = "String", - moduleName = ":mod", - flagType = LocalFlagEntry.FLAG_TYPE_REMOTE, - ) - assertEquals(false, entry.isLocal) - } -} diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt new file mode 100644 index 0000000..1c5651e --- /dev/null +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryTest.kt @@ -0,0 +1,32 @@ +package dev.androidbroadcast.featured.gradle + +import kotlin.test.Test +import kotlin.test.assertEquals + +class LocalFlagEntryTest { + @Test + fun `isLocal is true for local flagType`() { + val entry = + LocalFlagEntry( + key = "k", + defaultValue = "v", + type = "String", + moduleName = ":mod", + flagType = LocalFlagEntry.FLAG_TYPE_LOCAL, + ) + assertEquals(true, entry.isLocal) + } + + @Test + fun `isLocal is false for remote flagType`() { + val entry = + LocalFlagEntry( + key = "k", + defaultValue = "v", + type = "String", + moduleName = ":mod", + flagType = LocalFlagEntry.FLAG_TYPE_REMOTE, + ) + assertEquals(false, entry.isLocal) + } +} diff --git a/featured-registry/CLAUDE.md b/featured-registry/CLAUDE.md deleted file mode 100644 index 571191f..0000000 --- a/featured-registry/CLAUDE.md +++ /dev/null @@ -1,9 +0,0 @@ -# featured-registry - -See [README.md](README.md) for usage. - -## Commands - -```bash -./gradlew :featured-registry:allTests -``` diff --git a/featured-registry/README.md b/featured-registry/README.md deleted file mode 100644 index cce2027..0000000 --- a/featured-registry/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# featured-registry - -Runtime registry that tracks all `ConfigValue` instances across all modules in the app. - -Used by `featured-debug-ui` to enumerate every flag without manual registration. - -## How it works - -The `featured-gradle-plugin` task `generateFlagRegistrar` generates a `FlagRegistrar` class per module at build time. Each registrar registers its module's params into `FlagRegistry` at app startup. - -## Usage - -Add as `debugImplementation` — not needed in release builds. - -```kotlin -debugImplementation("dev.androidbroadcast.featured:featured-registry") -``` - -Pair with `featured-debug-ui`. No manual setup required beyond applying the Gradle plugin. diff --git a/featured-registry/build.gradle.kts b/featured-registry/build.gradle.kts deleted file mode 100644 index 559c71a..0000000 --- a/featured-registry/build.gradle.kts +++ /dev/null @@ -1,143 +0,0 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidKmpLibrary) - alias(libs.plugins.kover) - alias(libs.plugins.mavenPublish) - alias(libs.plugins.dokka) -} - -kotlin { - explicitApi() - jvmToolchain(21) - - compilerOptions { - freeCompilerArgs.add("-Xexpect-actual-classes") - } - - android { - namespace = "dev.androidbroadcast.featured.registry" - compileSdk = - libs.versions.android.compileSdk - .get() - .toInt() - minSdk = - libs.versions.android.minSdk - .get() - .toInt() - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "FeaturedRegistry" - isStatic = true - } - } - - jvm() - - @OptIn(ExperimentalKotlinGradlePluginApi::class) - applyDefaultHierarchyTemplate { - common { - group("jvmCommon") { - withJvm() - withCompilations { it.target.name == "android" } - } - group("native") { - withIosX64() - withIosArm64() - withIosSimulatorArm64() - } - } - } - - sourceSets { - commonMain.dependencies { - api(projects.core) - } - - @Suppress("unused") - val commonTest by getting { - dependencies { - implementation(libs.kotlin.test) - } - } - } -} - -mavenPublishing { - publishToMavenCentral() - signAllPublications() - coordinates( - groupId = "dev.androidbroadcast.featured", - artifactId = "featured-registry", - ) - pom { - name.set("Featured Registry") - description.set("Registry module for Featured – runtime flag registration and lookup for KMP") - inceptionYear.set("2024") - url.set("https://github.com/AndroidBroadcast/Featured") - licenses { - license { - name.set("MIT License") - url.set("https://opensource.org/licenses/MIT") - distribution.set("repo") - } - } - developers { - developer { - id.set("androidbroadcast") - name.set("Kirill Rozov") - url.set("https://github.com/androidbroadcast") - } - } - scm { - url.set("https://github.com/AndroidBroadcast/Featured") - connection.set("scm:git:git://github.com/AndroidBroadcast/Featured.git") - developerConnection.set("scm:git:ssh://git@github.com/AndroidBroadcast/Featured.git") - } - } -} - -kover { - reports { - filters { - excludes { - classes("*Test*", "*Mock*", "*Fake*") - } - } - - total { - html { - onCheck = false - } - - xml { - onCheck = false - } - - log { - onCheck = true - header = "Code coverage summary for :featured-registry module" - format = "Line coverage: %" - } - - verify { - onCheck = true - - rule { - minBound(85) - } - } - } - } -} diff --git a/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistry.kt b/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistry.kt deleted file mode 100644 index 4045a8e..0000000 --- a/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistry.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.androidbroadcast.featured.registry - -import dev.androidbroadcast.featured.ConfigParam - -/** - * Central registry that collects all [ConfigParam] instances across feature modules. - * Powers debug UI auto-discovery of available feature flags. - * - * Thread-safe: registration and retrieval use platform-specific synchronization - * (a lock on JVM/Android, CAS-based updates on Native/iOS). - */ -public object FlagRegistry { - private val delegate = FlagRegistryDelegate() - - /** - * Registers a [ConfigParam] with the registry. - * Duplicate registrations (same param by key equality) are silently ignored. - */ - public fun register(param: ConfigParam<*>) { - delegate.register(param) - } - - /** - * Returns an immutable snapshot of all currently registered [ConfigParam] instances. - */ - public fun all(): List> = delegate.all() - - /** - * Clears all registered params. Intended for use in tests only. - */ - internal fun reset() { - delegate.reset() - } -} diff --git a/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt b/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt deleted file mode 100644 index c76fdaf..0000000 --- a/featured-registry/src/commonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.androidbroadcast.featured.registry - -import dev.androidbroadcast.featured.ConfigParam - -internal expect class FlagRegistryDelegate() { - fun register(param: ConfigParam<*>) - - fun all(): List> - - fun reset() -} diff --git a/featured-registry/src/commonTest/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryTest.kt b/featured-registry/src/commonTest/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryTest.kt deleted file mode 100644 index abf5477..0000000 --- a/featured-registry/src/commonTest/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package dev.androidbroadcast.featured.registry - -import dev.androidbroadcast.featured.ConfigParam -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class FlagRegistryTest { - @BeforeTest - fun setUp() { - FlagRegistry.reset() - } - - @Test - fun registeredParamAppearsInAll() { - val param = ConfigParam(key = "flag_a", defaultValue = true) - FlagRegistry.register(param) - assertTrue(FlagRegistry.all().contains(param)) - } - - @Test - fun allReturnsAllRegisteredParams() { - val p1 = ConfigParam(key = "flag_b", defaultValue = false) - val p2 = ConfigParam(key = "flag_c", defaultValue = 42) - FlagRegistry.register(p1) - FlagRegistry.register(p2) - val all = FlagRegistry.all() - assertEquals(2, all.size) - assertTrue(all.contains(p1)) - assertTrue(all.contains(p2)) - } - - @Test - fun registeringDuplicateKeyDoesNotDuplicateEntry() { - val param = ConfigParam(key = "flag_d", defaultValue = "hello") - FlagRegistry.register(param) - FlagRegistry.register(param) - assertEquals(1, FlagRegistry.all().size) - } - - @Test - fun allReturnsEmptyWhenNothingRegistered() { - assertTrue(FlagRegistry.all().isEmpty()) - } - - @Test - fun registeringSameKeyWithDifferentDefaultValueDoesNotDuplicate() { - val param1 = ConfigParam(key = "flag_g", defaultValue = "first") - val param2 = ConfigParam(key = "flag_g", defaultValue = "second") - FlagRegistry.register(param1) - FlagRegistry.register(param2) - assertEquals(1, FlagRegistry.all().size) - } - - @Test - fun allReturnsImmutableSnapshot() { - val param = ConfigParam(key = "flag_e", defaultValue = 1) - FlagRegistry.register(param) - val snapshot = FlagRegistry.all() - FlagRegistry.register(ConfigParam(key = "flag_f", defaultValue = 2)) - assertEquals(1, snapshot.size) - } -} diff --git a/featured-registry/src/iosMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt b/featured-registry/src/iosMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt deleted file mode 100644 index d53a8eb..0000000 --- a/featured-registry/src/iosMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.androidbroadcast.featured.registry - -import dev.androidbroadcast.featured.ConfigParam -import kotlin.concurrent.AtomicReference - -internal actual class FlagRegistryDelegate actual constructor() { - // AtomicReference provides safe publication on Kotlin/Native new memory model. - // Copy-on-write: every register() replaces the list atomically via CAS. - private val paramsRef = AtomicReference>>(emptyList()) - - actual fun register(param: ConfigParam<*>) { - // Spin-loop CAS: add param if no entry with the same key exists. - while (true) { - val current = paramsRef.value - if (current.any { it.key == param.key }) return - val next = current + param - if (paramsRef.compareAndSet(current, next)) return - } - } - - actual fun all(): List> = paramsRef.value.toList() - - actual fun reset() { - paramsRef.value = emptyList() - } -} diff --git a/featured-registry/src/jvmCommonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt b/featured-registry/src/jvmCommonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt deleted file mode 100644 index b738de3..0000000 --- a/featured-registry/src/jvmCommonMain/kotlin/dev/androidbroadcast/featured/registry/FlagRegistryDelegate.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.androidbroadcast.featured.registry - -import dev.androidbroadcast.featured.ConfigParam -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock - -internal actual class FlagRegistryDelegate actual constructor() { - private val lock = ReentrantLock() - - // Keyed by ConfigParam.key to guarantee one entry per key across platforms. - private val params: LinkedHashMap> = LinkedHashMap() - - actual fun register(param: ConfigParam<*>) { - lock.withLock { params.putIfAbsent(param.key, param) } - } - - actual fun all(): List> = lock.withLock { params.values.toList() } - - actual fun reset() { - lock.withLock { params.clear() } - } -} diff --git a/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt b/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt index 7156e6b..817588d 100644 --- a/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt +++ b/sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt @@ -12,21 +12,18 @@ import androidx.compose.runtime.setValue import dev.androidbroadcast.featured.CheckoutVariant import dev.androidbroadcast.featured.ConfigValues import dev.androidbroadcast.featured.SampleApp +import dev.androidbroadcast.featured.SampleFeatureFlags import dev.androidbroadcast.featured.datastore.DataStoreConfigValueProvider import dev.androidbroadcast.featured.datastore.registerConverter import dev.androidbroadcast.featured.debugui.FeatureFlagsDebugScreen import dev.androidbroadcast.featured.enumConverter import dev.androidbroadcast.featured.platform.defaultLocalProvider -import dev.androidbroadcast.featured.registerSampleFlags class MainActivity : ComponentActivity() { // ConfigValues is held at Activity scope for this sample. // In production, move to Application or a DI singleton to avoid // recreating (and re-opening) the DataStore file on every rotation. private val configValues by lazy { - // Populate FlagRegistry so FeatureFlagsDebugScreen can discover all flags via FlagRegistry.all(). - registerSampleFlags() - val localProvider = defaultLocalProvider(applicationContext) // DataStore only handles primitives natively; register a converter so that the // enum-typed checkoutVariant flag can be persisted and observed without throwing. @@ -43,7 +40,7 @@ class MainActivity : ComponentActivity() { if (showDebug) { BackHandler { showDebug = false } - FeatureFlagsDebugScreen(configValues = configValues) + FeatureFlagsDebugScreen(configValues = configValues, registry = SampleFeatureFlags.all) } else { SampleApp( configValues = configValues, diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 9274e79..d2867b2 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -54,7 +54,6 @@ kotlin { // the public signatures of SampleApp / SampleViewModel — must be api to compile // downstream consumers like :sample:desktop. Pre-existing leak from #182. api(project(":core")) - implementation(project(":featured-registry")) } } } diff --git a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleApp.kt b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleApp.kt index a91be4c..56a9cc0 100644 --- a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleApp.kt @@ -4,22 +4,6 @@ package dev.androidbroadcast.featured import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import dev.androidbroadcast.featured.registry.FlagRegistry - -/** - * Registers all [SampleFeatureFlags] with [FlagRegistry] so that [FeatureFlagsDebugScreen] - * can discover them via [FlagRegistry.all]. Call once on application start before opening - * the debug UI. Duplicate calls are safe — the registry ignores already-registered params. - */ -public fun registerSampleFlags() { - listOf( - SampleFeatureFlags.mainButtonRed, - SampleFeatureFlags.newFeatureSectionEnabled, - SampleFeatureFlags.newCheckout, - SampleFeatureFlags.promoBannerEnabled, - SampleFeatureFlags.checkoutVariant, - ).forEach(FlagRegistry::register) -} /** * Root composable for the sample application. diff --git a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt index b75c4f5..757a3bd 100644 --- a/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt +++ b/sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleFeatureFlags.kt @@ -41,6 +41,7 @@ public enum class CheckoutVariant { * * The sample module is part of the library's own build and cannot apply the plugin * to itself, so all flags are declared manually here for demonstration purposes. + * [SampleFeatureFlags.all] is the single source of truth for the Debug UI registry. */ public object SampleFeatureFlags { public val mainButtonRed: ConfigParam = @@ -81,4 +82,13 @@ public object SampleFeatureFlags { description = "Controls which checkout flow variant is shown to the user", category = "checkout", ) + + public val all: List> = + listOf( + mainButtonRed, + newFeatureSectionEnabled, + newCheckout, + promoBannerEnabled, + checkoutVariant, + ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 201871b..1b45cb1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,7 +42,6 @@ include(":sample:android-app") include(":sample:desktop") include(":core") include(":featured-compose") -include(":featured-registry") include(":featured-debug-ui") include(":featured-testing")