Per-module ConfigValues + internal generated objects#202
Conversation
Flip the visibility of generated GeneratedLocalFlags* and GeneratedRemoteFlags* objects from public to internal. A module's flag declarations are an implementation detail; cross-module introspection flows exclusively through GeneratedFeaturedRegistry.all. Update ConfigParamGenerator KDoc, ExtensionFunctionGenerator KDoc, and ConfigParamGeneratorTest to match the new contract.
…Values Each :sample:feature-* module now owns its ViewModel and ConfigValues: - CheckoutFlagsViewModel in :sample:feature-checkout - PromotionsFlagsViewModel in :sample:feature-promotions - UiFlagsViewModel + MainButtonColor in :sample:feature-ui SampleApp/FeaturedSample accept three per-feature VMs as parameters instead of a single shared ConfigValues. Platform shells (Activity, desktop main, iOS UIViewController) construct four ConfigValues from one shared local provider and pass them to the appropriate VMs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UiFlagsViewModel.mainButtonColor: fix initial value Blue → Red to match flagActive's true default (no cold-start flicker) - Desktop and iOS shells: add debugConfigValues for pattern consistency per plan §10.3/§10.4
- Revert Phase A finding 3: dead Desktop/iOS debugConfigValues vars; doc-only note that those shells omit the debug aggregator since they have no debug-UI entry - FeaturedSample: internal (only used by SampleApp wrapper in same module) - MainButton: private (only used in same file) - MainViewController.kt: drop RedundantVisibilityModifier suppression (public required by explicit-API mode) - sample/CLAUDE.md: clarify debug aggregator is Android-only
- UiFlagsViewModel: remove flagActive public API (dual-exposure of Boolean + MainButtonColor); make setter accept MainButtonColor instead of Boolean for symmetric vocabulary - MainButtonColor: drop unused companion object Default (contradicted ViewModel's initialValue) - FeaturedSample: derive activate locally from buttonColor, call setMainButtonColor with domain type - ConfigParamGeneratorTest: add symmetric remote-public-val assertion (parity with local)
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Pull request overview
This PR reinforces module ownership in Featured by making generated flag objects module-internal and updating the sample app to wire one ConfigValues per feature module (plus an Android-only debug aggregator) over a shared provider.
Changes:
- Gradle plugin codegen now emits
internal object GeneratedLocalFlags*/GeneratedRemoteFlags*and removes explicitpublicproperty modifiers. - Sample app is refactored from a single shared
SampleViewModelto three per-feature*FlagsViewModels, with platform shells constructing per-featureConfigValues. - Documentation and changelog are updated to describe the multi-module wiring pattern.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sample/shared/src/iosMain/kotlin/dev/androidbroadcast/featured/MainViewController.kt | iOS entry point now constructs per-feature ConfigValues + per-feature ViewModels over a shared provider. |
| sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleViewModel.kt | Removes the previous shared ViewModel that mixed multiple modules’ flags. |
| sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/SampleApp.kt | Updates the public composition root to accept per-feature ViewModels instead of a single ConfigValues. |
| sample/shared/src/commonMain/kotlin/dev/androidbroadcast/featured/FeaturedSample.kt | Refactors UI to read per-feature ViewModels; makes FeaturedSample internal; adjusts MainButton API usage. |
| sample/shared/build.gradle.kts | Adjusts dependencies/comments to reflect new public API surface relying on per-feature modules. |
| sample/feature-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/ui/UiFlagsViewModel.kt | Adds UI feature ViewModel that maps UI flags into StateFlows and setters. |
| sample/feature-ui/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/ui/MainButtonColor.kt | Extracts MainButtonColor type into the UI feature module. |
| sample/feature-ui/build.gradle.kts | Adds lifecycle-viewmodel dependency for the new ViewModel class. |
| sample/feature-promotions/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/promotions/PromotionsFlagsViewModel.kt | Adds promotions feature ViewModel for the remote flag state + setter. |
| sample/feature-promotions/build.gradle.kts | Adds lifecycle-viewmodel dependency for the new ViewModel class. |
| sample/feature-checkout/src/commonMain/kotlin/dev/androidbroadcast/featured/sample/checkout/CheckoutFlagsViewModel.kt | Adds checkout feature ViewModel for checkout-related flags + setter. |
| sample/feature-checkout/build.gradle.kts | Adds lifecycle-viewmodel dependency for the new ViewModel class. |
| sample/desktop/src/jvmMain/kotlin/dev/androidbroadcast/featured/Main.Desktop.kt | Desktop entry point now constructs per-feature ConfigValues + ViewModels over a shared provider. |
| sample/CLAUDE.md | Updates sample module architecture/wiring documentation for per-module ConfigValues. |
| sample/android-app/src/main/kotlin/dev/androidbroadcast/featured/sample/MainActivity.kt | Android shell now wires shared provider + 3 per-feature ConfigValues + debug aggregator and scopes ViewModels to Activity. |
| sample/android-app/build.gradle.kts | Adds lifecycle-viewmodel-compose dependency for viewModel { } usage. |
| README.md | Documents the multi-module wiring pattern and debug aggregator approach. |
| gradle/libs.versions.toml | Adds version-catalog entry for androidx-lifecycle-viewmodel. |
| core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValues.kt | Adds KDoc describing the multi-module wiring contract and shared-provider propagation. |
| CHANGELOG.md | Records the breaking change: generated flag objects are now module-internal; sample demonstrates per-module wiring. |
| build-logic/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt | Updates tests to assert internal object generation and absence of explicit public val. |
| build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGenerator.kt | Updates documentation to reflect that generated objects are now internal within a module. |
| build-logic/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt | Changes generator output to internal object and removes explicit public modifiers on properties. |
|
|
||
| ```kotlin | ||
| // Construct one ConfigValues per feature module + one debug aggregator, all over a shared provider | ||
| val sharedLocal: LocalConfigValueProvider = DataStoreConfigValueProvider(context, …) |
There was a problem hiding this comment.
Fixed in 86aa880 — see review fix commit.
| // :core types (ConfigValues, InMemoryConfigValueProvider) appear in | ||
| // the public signatures of SampleApp — must be api to compile | ||
| // downstream consumers like :sample:desktop. Pre-existing leak from #182. |
There was a problem hiding this comment.
Fixed in 86aa880 — see review fix commit.
| // VMs are constructed once per UIViewController — ConfigValues lifetimes are tied to the | ||
| // view controller, which is the iOS equivalent of the Application scope in this sample. |
There was a problem hiding this comment.
Fixed in 86aa880 — see review fix commit.
- README: snippet uses defaultLocalProvider(applicationContext) instead of incorrect DataStoreConfigValueProvider(context, ...) (real signature takes DataStore<Preferences>) - sample/shared/build.gradle.kts: rewrite stale api(:core) rationale — :core is referenced from iosMain (MainViewController) and via per-feature VM constructors, not SampleApp's signature - MainViewController.kt: drop misleading "iOS equivalent of Application scope" wording; UIViewController lifetime is screen/session, not app-wide
Summary
Restructures the library so each feature module owns its own
ConfigValuesinstance with only that module's flags. The aggregated "everything" instance exists only as a debug-screen aggregator. Implemented in two layers:GeneratedLocalFlags*/GeneratedRemoteFlags*objects are nowinternalto their declaring Gradle module. Cross-module flag introspection flows exclusively throughGeneratedFeaturedRegistry.all(built from per-module manifests by the aggregator plugin).SampleViewModelsplit into three per-feature*FlagsViewModels (each in its own:sample:feature-*module). Android shell builds 3 productionConfigValues+ 1 debug aggregator; Desktop and iOS build 3 (no debug-UI entry).Why
The previous shape exposed every module's
GeneratedLocalFlags*aspublic, letting any module read any other module's flags by typing its name. That defeats the modular-ownership story Featured is supposed to demonstrate. Sharing a singleConfigValuesacross the sample reinforced the leak. The new shape: each module's flag declarations are an implementation detail, exposed to other modules only via the public observe-bridge extensions and the per-featureViewModel.What changed
Library
build-logic/featured-gradle-plugin/.../ConfigParamGenerator.kt— emitsinternal object GeneratedLocalFlagsX/internal object GeneratedRemoteFlagsX(waspublic object); object property declarations drop the explicitpublicmodifier.core/.../ConfigValues.kt— KDoc adds the multi-module wiring contract: sharedLocalConfigValueProvideris the single source of truth for overrides; per-module instances see writes via the provider's reactiveobserve.GeneratedFeaturedRegistryGenerator), R8 rules (ProguardRulesGenerator), extension functions (ExtensionFunctionGenerator) — unchanged. Verified: aggregator already pre-constructsConfigParam(...)inline from manifest data and does not reference foreign-module symbols.Sample
sample/feature-checkout/.../CheckoutFlagsViewModel.kt,sample/feature-promotions/.../PromotionsFlagsViewModel.kt,sample/feature-ui/.../UiFlagsViewModel.kt— per-featureViewModels, each taking only its module'sConfigValues. Live in their respective:sample:feature-*modules so they can see the now-internalgenerated objects.sample/shared/.../SampleViewModel.kt— deleted.sample/feature-ui/.../MainButtonColor.kt— sealed interface extracted from oldSampleViewModel.sample/android-app/.../MainActivity.kt— constructs 4ConfigValuesover one sharedDataStoreConfigValueProvider(3 per-feature + 1 debug aggregator); wires 3 per-feature VMs into Compose; passesdebugConfigValuestoFeatureFlagsDebugScreen.sample/desktop/.../Main.Desktop.kt,sample/shared/src/iosMain/.../MainViewController.kt— 3 per-featureConfigValuesover oneInMemoryConfigValueProvider; no debug aggregator (no debug-UI entry).sample/shared/.../FeaturedSample.kt/SampleApp.kt— composition root takes per-feature VMs;FeaturedSampleisinternal(only consumed bySampleAppin the same module).Tests / build
gradle/libs.versions.toml— addedandroidx-lifecycle-viewmodel(catalog entry, reuses existingandroidx-lifecycleversion ref).sample/feature-*/build.gradle.kts—api(libs.androidx.lifecycle.viewmodel)on each feature module (VM types are in their public surface).sample/android-app/build.gradle.kts—implementation(libs.androidx.lifecycle.viewmodelCompose)forviewModel { … }.ConfigParamGeneratorTest.kt— assertsinternal objectfor both Local and Remote; asserts no explicitpublic valon object properties (symmetric for both variants).Docs
README.md— new "Multi-module pattern" section with code snippet.sample/CLAUDE.md— multi-module wiring note (Android-only debug aggregator).CHANGELOG.md—Unreleasedentry underChanged.Breaking changes
Featured does not keep API compatibility (per project policy). External consumers that referenced
GeneratedLocalFlagsX.xfrom a foreign module will not compile. The migration path is one of: (a) depend on the declaring module and access via that module's observe-bridge extensions, or (b) for cross-module flag listing, useGeneratedFeaturedRegistry.all.How to test
Manual QA scenario covered on Pixel 10 AVD (mandatory per project convention), Desktop visual, iOS sim build. 16/19 scenarios PASS, 3 blocked by Mac screen lock (Desktop visual — same known PR D limitation, not a code defect). Critical cross-instance propagation test (
debugConfigValues.override(main_button_red, off)→uiConfigValues.observere-emits → MainButton turns blue) confirmed working.See
swarm-report/per-module-configvalues-e2e-scenario.mdfor the executed plan.Pre-merge automated gates
./gradlew -p build-logic :featured-gradle-plugin:test— green../gradlew spotlessApply :sample:android-app:assembleDebug :sample:desktop:assemble :sample:shared:linkDebugFrameworkIosSimulatorArm64— green.Status
Artifacts
swarm-report/per-module-configvalues-plan.mdswarm-report/per-module-configvalues-state.mdswarm-report/per-module-configvalues-e2e-scenario.mdswarm-report/per-module-configvalues-finalize.mdRelease Notes
Changed
GeneratedLocalFlagsX/GeneratedRemoteFlagsXobjects are nowinternalto their declaring Gradle module — flag declarations no longer leak across module boundaries. Cross-module flag listing flows exclusively throughGeneratedFeaturedRegistry.all.ConfigValuesper feature module plus a dedicated debug aggregator, all sharing the sameLocalConfigValueProvider.Checklist
[Unreleased]ConfigValuesKDoc carries the shared-provider override-propagation contractinternalcodegen contractinternalgenerated objects)🤖 Generated with Claude Code