Skip to content

Per-module ConfigValues + internal generated objects#202

Merged
kirich1409 merged 8 commits into
developfrom
feat/per-module-configvalues
May 20, 2026
Merged

Per-module ConfigValues + internal generated objects#202
kirich1409 merged 8 commits into
developfrom
feat/per-module-configvalues

Conversation

@kirich1409
Copy link
Copy Markdown
Contributor

Summary

Restructures the library so each feature module owns its own ConfigValues instance with only that module's flags. The aggregated "everything" instance exists only as a debug-screen aggregator. Implemented in two layers:

  • CodegenGeneratedLocalFlags* / GeneratedRemoteFlags* objects are now internal to their declaring Gradle module. Cross-module flag introspection flows exclusively through GeneratedFeaturedRegistry.all (built from per-module manifests by the aggregator plugin).
  • SampleSampleViewModel split into three per-feature *FlagsViewModels (each in its own :sample:feature-* module). Android shell builds 3 production ConfigValues + 1 debug aggregator; Desktop and iOS build 3 (no debug-UI entry).

Why

The previous shape exposed every module's GeneratedLocalFlags* as public, 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 single ConfigValues across 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-feature ViewModel.

What changed

Library

  • build-logic/featured-gradle-plugin/.../ConfigParamGenerator.kt — emits internal object GeneratedLocalFlagsX / internal object GeneratedRemoteFlagsX (was public object); object property declarations drop the explicit public modifier.
  • core/.../ConfigValues.kt — KDoc adds the multi-module wiring contract: shared LocalConfigValueProvider is the single source of truth for overrides; per-module instances see writes via the provider's reactive observe.
  • Aggregator (GeneratedFeaturedRegistryGenerator), R8 rules (ProguardRulesGenerator), extension functions (ExtensionFunctionGenerator) — unchanged. Verified: aggregator already pre-constructs ConfigParam(...) 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-feature ViewModels, each taking only its module's ConfigValues. Live in their respective :sample:feature-* modules so they can see the now-internal generated objects.
  • sample/shared/.../SampleViewModel.kt — deleted.
  • sample/feature-ui/.../MainButtonColor.kt — sealed interface extracted from old SampleViewModel.
  • sample/android-app/.../MainActivity.kt — constructs 4 ConfigValues over one shared DataStoreConfigValueProvider (3 per-feature + 1 debug aggregator); wires 3 per-feature VMs into Compose; passes debugConfigValues to FeatureFlagsDebugScreen.
  • sample/desktop/.../Main.Desktop.kt, sample/shared/src/iosMain/.../MainViewController.kt — 3 per-feature ConfigValues over one InMemoryConfigValueProvider; no debug aggregator (no debug-UI entry).
  • sample/shared/.../FeaturedSample.kt / SampleApp.kt — composition root takes per-feature VMs; FeaturedSample is internal (only consumed by SampleApp in the same module).

Tests / build

  • gradle/libs.versions.toml — added androidx-lifecycle-viewmodel (catalog entry, reuses existing androidx-lifecycle version ref).
  • sample/feature-*/build.gradle.ktsapi(libs.androidx.lifecycle.viewmodel) on each feature module (VM types are in their public surface).
  • sample/android-app/build.gradle.ktsimplementation(libs.androidx.lifecycle.viewmodelCompose) for viewModel { … }.
  • ConfigParamGeneratorTest.kt — asserts internal object for both Local and Remote; asserts no explicit public val on 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.mdUnreleased entry under Changed.

Breaking changes

Featured does not keep API compatibility (per project policy). External consumers that referenced GeneratedLocalFlagsX.x from 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, use GeneratedFeaturedRegistry.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.observe re-emits → MainButton turns blue) confirmed working.

See swarm-report/per-module-configvalues-e2e-scenario.md for 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

Stage Result
Plan (design proposal, user-reviewed) PASS
Codegen change PASS
Sample restructure PASS
Docs / CHANGELOG PASS
Manual QA (Pixel 10, Desktop, iOS sim build) PASS (3 Desktop visual scenarios blocked by env, not code)
/finalize Round 1 (A code-reviewer + B simplify + C pr-review-toolkit trio) PASS

Artifacts

  • Plan: swarm-report/per-module-configvalues-plan.md
  • State: swarm-report/per-module-configvalues-state.md
  • QA scenario: swarm-report/per-module-configvalues-e2e-scenario.md
  • Finalize report: swarm-report/per-module-configvalues-finalize.md

Release Notes

Changed

  • Breaking: Generated GeneratedLocalFlagsX / GeneratedRemoteFlagsX objects are now internal to their declaring Gradle module — flag declarations no longer leak across module boundaries. Cross-module flag listing flows exclusively through GeneratedFeaturedRegistry.all.
  • Sample app demonstrates the per-module wiring pattern: one ConfigValues per feature module plus a dedicated debug aggregator, all sharing the same LocalConfigValueProvider.

Checklist

  • CHANGELOG updated under [Unreleased]
  • README documents the multi-module pattern
  • ConfigValues KDoc carries the shared-provider override-propagation contract
  • Plugin unit tests cover the new internal codegen contract
  • Manual QA on Pixel 10 emulator
  • iOS framework link verified (Swift surface has zero references to internal generated objects)
  • Desktop visual verification (blocked by Mac screen lock during QA — re-run when display available)

🤖 Generated with Claude Code

kirich1409 and others added 7 commits May 20, 2026 13:18
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)
@kirich1409 kirich1409 added enhancement New feature or request gradle-plugin featured-gradle-plugin work docs Documentation sample Sample app labels May 20, 2026
@kirich1409 kirich1409 marked this pull request as ready for review May 20, 2026 12:32
Copilot AI review requested due to automatic review settings May 20, 2026 12:32
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 23 files

Re-trigger cubic

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 explicit public property modifiers.
  • Sample app is refactored from a single shared SampleViewModel to three per-feature *FlagsViewModels, with platform shells constructing per-feature ConfigValues.
  • 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.

Comment thread README.md Outdated

```kotlin
// Construct one ConfigValues per feature module + one debug aggregator, all over a shared provider
val sharedLocal: LocalConfigValueProvider = DataStoreConfigValueProvider(context, …)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86aa880 — see review fix commit.

Comment thread sample/shared/build.gradle.kts Outdated
Comment on lines 57 to 59
// :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.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86aa880 — see review fix commit.

Comment on lines +24 to +25
// 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.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@kirich1409 kirich1409 merged commit 42fa81c into develop May 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation enhancement New feature or request gradle-plugin featured-gradle-plugin work sample Sample app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants