Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,4 @@ captures
.pipeline/
*.preferences_pb
/docs/superpowers/
/docs/adr/
/.claude/worktrees/
21 changes: 21 additions & 0 deletions docs/adr/001-configvalues-multi-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ADR 001: ConfigValues in Multi-Module Projects

**Status:** Accepted

## Context

In multi-module Android/KMP projects, each feature module may declare its own `ConfigParam` flags. The question is how to compose these into a single `ConfigValues` instance that is shared across the app.

## Decision

Each module declares its flags as top-level `ConfigParam` constants. A single `ConfigValues` instance is created at the app level (e.g., in the DI graph) by passing all params through the same providers.

The Gradle plugin generates a `FlagRegistrar` per module. The app module aggregates them via the generated `GeneratedFlagRegistry`, which collects every module's registrar at compile time.

Modules never create their own `ConfigValues`; they only declare params and read from the shared instance injected from the app layer.

## Consequences

- Flag namespacing is flat — param names must be unique across all modules.
- There is one set of local/remote providers for the whole app; per-module providers are not supported.
- The Gradle plugin enforces uniqueness and generates the aggregation boilerplate automatically.
Comment on lines +7 to +21
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The ADR’s “Decision” section describes flags as top-level ConfigParam constants and references an app-level GeneratedFlagRegistry. In this repo, flags are typically declared via the featured { } Gradle DSL and the plugin generates Generated{Local,Remote}Flags plus a per-module GeneratedFlagRegistrar; there doesn’t appear to be any GeneratedFlagRegistry type in the codebase. Please update the ADR to reflect the actual generated artifacts and how multi-module apps should wire things together (and avoid referencing non-existent symbols).

Suggested change
In multi-module Android/KMP projects, each feature module may declare its own `ConfigParam` flags. The question is how to compose these into a single `ConfigValues` instance that is shared across the app.
## Decision
Each module declares its flags as top-level `ConfigParam` constants. A single `ConfigValues` instance is created at the app level (e.g., in the DI graph) by passing all params through the same providers.
The Gradle plugin generates a `FlagRegistrar` per module. The app module aggregates them via the generated `GeneratedFlagRegistry`, which collects every module's registrar at compile time.
Modules never create their own `ConfigValues`; they only declare params and read from the shared instance injected from the app layer.
## Consequences
- Flag namespacing is flat — param names must be unique across all modules.
- There is one set of local/remote providers for the whole app; per-module providers are not supported.
- The Gradle plugin enforces uniqueness and generates the aggregation boilerplate automatically.
In multi-module Android/KMP projects, each feature module may declare its own flags via the `featured { }` Gradle DSL. The question is how to compose these generated flags into a single `ConfigValues` instance that is shared across the app.
## Decision
Each module declares its flags in the `featured { }` Gradle DSL. The Gradle plugin generates module-scoped accessors such as `GeneratedLocalFlags` and `GeneratedRemoteFlags`, along with a per-module `GeneratedFlagRegistrar`.
A single `ConfigValues` instance is created at the app level (for example, in the DI graph) and is wired using the set of `GeneratedFlagRegistrar` instances from the modules that participate in the app. There is no app-level generated `GeneratedFlagRegistry`; multi-module apps must explicitly include each module's generated registrar in their app wiring.
Modules never create their own `ConfigValues`; they declare flags through the plugin-generated model and read from the shared instance injected from the app layer.
## Consequences
- Flag namespacing is flat — flag names must be unique across all modules.
- There is one set of local/remote providers for the whole app; per-module providers are not supported.
- The Gradle plugin generates `GeneratedLocalFlags`, `GeneratedRemoteFlags`, and a `GeneratedFlagRegistrar` for each module, while the app is responsible for wiring the relevant registrars together.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The consequences list says “The Gradle plugin enforces uniqueness” and “generates the aggregation boilerplate automatically”. From the current plugin implementation, resolveFeatureFlags just serializes each module’s flags and scanAllLocalFlags only depends on per-module tasks; there’s no cross-module duplicate-key validation or generated app-level aggregator. Please reword these bullets to match actual behavior (or add explicit validation/generation if that’s intended).

Suggested change
- The Gradle plugin enforces uniqueness and generates the aggregation boilerplate automatically.
- The Gradle plugin currently serializes each module's declared flags, but it does not enforce cross-module uniqueness or generate an app-level aggregation boilerplate automatically.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue
import dev.androidbroadcast.featured.ConfigParam
import dev.androidbroadcast.featured.ConfigValue
import dev.androidbroadcast.featured.RemoteConfigValueProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.tasks.await
import kotlin.reflect.KClass

Expand Down Expand Up @@ -98,7 +99,7 @@ public class FirebaseConfigValueProvider(

try {
task.await()
} catch (e: RuntimeException) {
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
throw FetchException("Firebase Remote Config fetch failed", e)
Comment on lines 100 to 105
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

fetch() now treats CancellationException specially (rethrowing it), but the firebase provider tests only assert that failures are wrapped in FetchException. Please add a unit test covering the cancellation path (e.g., cancelled Task or coroutine cancellation) to ensure cancellation is not accidentally wrapped in FetchException in the future.

Copilot uses AI. Check for mistakes.
Expand Down
Loading