Skip to content
Back to archipelago.banuacoder.com

Feature SDKs (Enterprise)

app_debugger_feature_flag Enterprise ​

Tier
Enterpriseversion 2.0.2
Command
archipelago generate app_debugger_feature_flag
Generates
  • features/app_debugger_feature_flag/app_debugger_feature_flag
  • features/app_debugger_feature_flag/app_debugger_feature_flag_api
  • features/app_debugger_feature_flag/app_debugger_feature_flag_noop
Generates first
app_debuggerwhen needed and missing, so there is nothing to install first
Depends on
shared_identifiers, feature_flag_sdkgenerate these first

Feature flag spoofer sub-brick for app_debugger. Adds a Feature Flags tool to the debugger that lists every flag your providers know and lets developers override them at runtime, without touching the real provider chain. Automatically swapped to a no-op stub in production-flavor builds via build_prepare.yaml.

The Feature Flags tool in light mode: a search field, the spoofer switch, filter chips with counts, and flags grouped by source with an Overridden badge, a source label, a typed value and an inline switch for bool flags.

The same list in dark mode.

Variables ​

VariableDefaultDescription
uiKitPackageNameapp_ui_kitThe project's UI kit package. Read from archipelago.yaml in pre_gen.
prefixAppThe UI kit class prefix (AppTextField). Read from archipelago.yaml in pre_gen.

The CLI fills both from archipelago.yaml. Pass them only to raw mason make in CI.

Prerequisites ​

  • app_debugger, generated first when the project lacks it. It provides app_debugger_api (the DebuggerPanelPageRegistry contract) and the di/debug_tools.dart sentinels this brick's post_gen.dart patches (moved off the host app's bootstrap.dart in 1.1.1 — see below).
  • A feature_flag_impl package configured in your app for real flag evaluation.
  • feature_flag_sdk 2.1.0 or newer. The spoofer lists flags through FeatureFlagSnapshotSource in feature_flag_api. On an older feature_flag_api, pre_gen stops before it writes anything and asks you to upgrade feature_flag_sdk first.

Usage ​

Interactive ​

bash
archipelago generate app_debugger_feature_flag

Non-interactive (CI) ​

bash
archipelago generate app_debugger_feature_flag --config my_config.json

Generated Structure ​

text
features/
└── app_debugger_feature_flag/
    ├── app_debugger_feature_flag_api/
    │   └── lib/src/
    │       ├── feature_flag_spoofer_sdk.dart    # FeatureFlagSpooferSDK contract
    │       ├── spoofer_ui_kit.dart              # SpooferUiKit port
    │       ├── material_spoofer_ui_kit.dart     # Material fallback, extend it
    │       └── models/flag_override.dart        # FlagOverride model
    ├── app_debugger_feature_flag/
    │   └── lib/src/
    │       ├── feature_flag_spoofer_sdk_impl.dart
    │       ├── feature_flag_spoofer_delegate.dart
    │       ├── feature_flag_spoofer_page.dart   # "Feature Flags" debugger tool
    │       ├── kit_spoofer_ui_kit.dart          # default SpooferUiKit on your UI kit
    │       └── di/injector.dart
    └── app_debugger_feature_flag_noop/
        └── lib/src/di/injector.dart             # production-flavor stub, hand-written (no codegen)

API ​

dart
abstract interface class FeatureFlagSpooferSDK implements FeatureSDK {
  bool get isActive;
  Stream<List<FlagOverride>> get overridesStream;
  List<FlagOverride> get currentOverrides;
  void setOverride(FlagOverride override);
  void removeOverride(String flagKey);
  void clearAllOverrides();
  bool get isSpoofing;                  // false: overrides kept, not applied
  void setSpoofing({required bool enabled});
  Future<Map<String, FlagSnapshot>> snapshot();
  Future<void> fetchNow();
  FeatureFlagDelegate? get delegate;
}

class FlagOverride {
  const FlagOverride({required this.flagKey, required this.value, this.enabled = true});
  final String flagKey;
  final FeatureFlagResult value;
  final bool enabled; // false suspends the override without removing it
}

Since 1.1.1, post_gen.dart wires the delegate for you — no manual step required. It injects this into app_debugger's own di/debug_tools.dart, inside a dedicated archipelago:feature_flag_spoofer_delegate sentinel that runs right after the eager debug_panel_pages resolution:

dart
final spooferDelegate = GetIt.I<FeatureFlagSpooferSDK>().delegate;
if (spooferDelegate != null) {
  FeatureFlagDelegateRegistry.instance.register(spooferDelegate);
}

FeatureFlagDelegateRegistry lives in feature_flag_api — this brick's hook adds feature_flag_api (not feature_flag_impl) as a dependency of app_debugger, so a debug-only package never pulls in a release impl. FeatureFlagManager (from feature_flag_impl) reads the registry on every evaluation, so the delegate applies whenever it registers. The null guard is what makes this safe in release builds, where the noop stub's delegate getter returns null — so nothing is registered in production.

How it works ​

  1. FeatureFlagSpooferSDKImpl (@Singleton(as: FeatureFlagSpooferSDK), eager since 1.2.1) constructs a FeatureFlagSpooferDelegate backed by an in-memory Map<String, FlagOverride> and registers a FeatureFlagSpooferPage with DebuggerPanelPageRegistry — the debugger home lists it as a tool without ever importing this package.
  2. FeatureFlagDelegate is an override hook that runs before the real provider chain; FeatureFlagSpooferDelegate.evaluate returns override.value for an enabled override, or null to defer to the next delegate/real provider.
  3. The panel page lists every flag the providers know. snapshot() reads each provider registered in FeatureFlagProviderRegistry that implements FeatureFlagSnapshotSource and merges the results in registration order; the first provider that knows a key wins. A provider that throws is skipped, so one unreachable vendor does not hide the others. See The Feature Flags tool.
  4. build_prepare.yaml swaps app_debugger_feature_flag → app_debugger_feature_flag_noop for production-flavor builds, stripping the spoofer entirely.

The Feature Flags tool ​

  • List: flags grouped by source (Remote, Cached, Default). Each row shows the key, an Overridden badge, the source (remote, cache, default) and the value with its type (bool, number, text, json). The type is the provider's FlagSnapshot.valueType. For a key no provider reports, it is inferred from the override.
  • Search matches keys and values. Filter chips: All, Overridden, Remote (includes cached values), Default, each with a count.
  • Bool flags have a switch in the row. So does a JSON object with exactly one bool field, such as {"enabled": true}: the switch toggles that field. The switch shows the value the app gets. Switching back to the provider's value removes the override, so a flag is either remote or overridden, never stale. A paused or reset override shows the provider value.
  • Other flags open a full-screen editor: the key as the title with a copy button, Save in the app bar, and Reset to remote value at the bottom, off when the flag is not overridden. JSON is pretty-printed in a monospace editor with a Format button. An invalid document shows the parse error with its line and column below the field, and Save stays off. Numbers get a numeric field, text a multiline field. Saving an unchanged value shows "No changes".
  • Spoofer switch: off keeps every override but stops applying it. The list shows provider values and is view-only: editing shows "Spoofer is off".
  • Reset per flag and Reset all, each with Undo. Fetch now calls fetchNow() on every provider and lists the flags again.
  • Persistence: overrides and the spoofer switch are saved to feature_flag_overrides.json in the app support directory and restored in initialize(), before the first flag evaluation. Without a file system (unit tests) they stay in memory.

The full-screen JSON editor for app_config, showing an inline parse error with its line and column, and Save turned off.

The JSON editor in dark mode, with the value pretty-printed, a Format button and Reset to remote value at the bottom.

Match your design system: SpooferUiKit ​

The tool draws every control through SpooferUiKit, a port in app_debugger_feature_flag_api. The page builds only layout (rows, columns, padding); every visible control comes from the port.

MethodDraws
openPage(context, builder)Pushes the full-screen editor
page(context, title:, actions:, body:, bottom:)The editor page: app bar with title and actions, body, pinned bottom action
showMessage(context, message, actionLabel:, onAction:)Short feedback, with Undo
searchField(context, hint:, onChanged:)The search field
textField(context, controller:, label:, errorText:, multiline:, monospace:, keyboardType:, onChanged:)The editor field, JSON included
filterChip(context, label:, selected:, onSelected:)One single-select filter
toggle(context, value:, onChanged:, semanticLabel:)Row switches and the spoofer switch
pill(context, text:, label:, tone:)Source, typed value and the Overridden badge (SpooferPillTone.highlight)
sectionHeader(context, label)A source group header
note(context, text)Counts, hints, empty state
flagTile(context, title:, badges:, onTap:, trailing:)One flag row
button(context, label:, onPressed:, kind:)SpooferButtonKind.primary, secondary or destructive
iconButton(context, icon:, tooltip:, onPressed:)Fetch now, reset, copy
loadingBar(context, loading:)The divider that shows fetch progress

Without a registration the page uses KitSpooferUiKit, generated into app_debugger_feature_flag: your UI kit's text field for search, kit tokens (context.colors, context.spacing, context.typography, context.radius) for pills, headers and notes, and Material styled by your kit theme for the rest. It uses only the kit widgets and tokens the debugger home already uses, so it compiles wherever app_debugger does.

Write your own adapter ​

Extend MaterialSpooferUiKit and override only what your design system draws differently, then register it in the root GetIt before the debugger opens:

dart
class MySpooferUiKit extends MaterialSpooferUiKit {
  const MySpooferUiKit();

  @override
  Widget filterChip(
    BuildContext context, {
    required String label,
    required bool selected,
    required VoidCallback onSelected,
  }) => MyPill(label: label, selected: selected, onTap: onSelected);

  @override
  Widget toggle(
    BuildContext context, {
    required bool value,
    required ValueChanged<bool> onChanged,
    required String semanticLabel,
  }) => MySwitch(value: value, onChanged: onChanged, label: semanticLabel);
}

// In your app's DI setup:
getIt.registerSingleton<SpooferUiKit>(const MySpooferUiKit());

Generation adds the _api package to your app's pubspec.yaml, so the app can import it.

Production-flavor builds keep the _api package, so the registration compiles in every flavor. Return widgets that take their colors from your theme, and keep tap targets at 48dp or more.

Adapter notes:

  • If your kit's button awaits haptic feedback before it calls onPressed, widget tests that tap it hang. Turn haptics off in tests, or do not await them.
  • The pill text is already clipped to one short line (about 32 characters, ending in an ellipsis) by the page, so your pill does not need to ellipsize. The full value stays in the editor.
  • MaterialSpooferUiKit.page adds trailing room after the app bar actions, so Save does not sit in the screen corner under the debug banner. A kit that overrides page should do the same.

DI and wiring notes ​

app_debugger_feature_flag/pubspec.yaml declares an archipelago: manifest block:

yaml
archipelago:
  di_modules:
    - class: AppDebuggerFeatureFlagPackageModule

so monorepo_toolkit generate host-registry can rediscover and re-wire the module after the initial post_gen pass. FeatureFlagSpooferSDKImpl is registered as an eager @Singleton (a lazy one would join FeatureSDKRegistry too late and the Feature Flags tool would never appear). post_gen.dart also patches the archipelago:debug_panel_pages sentinel in app_debugger's own di/debug_tools.dart (not the host app's bootstrap.dart — moved there in 1.1.1) to resolve it before first frame; verify it's present if the tool is missing.

post_gen.dart also appends a second, dedicated archipelago:feature_flag_spoofer_delegate sentinel right after the debug_panel_pages block, registering the delegate as shown in the API section above — so both the panel tab and the override delegate are wired automatically on generation, with no follow-up step in your own app code.

The noop side (app_debugger_feature_flag_noop) is hand-written directly in injector.dart with a matching AppDebuggerFeatureFlagPackageModule class name and an empty init() — as of 1.1.2 there is no codegen and no FeatureFlagSpooferNoopSDKImpl class, since FeatureFlagSpooferSDK is only ever resolved from inside app_debugger's own production-stripped composition.

Dependencies ​

  • app_debugger_api (path dependency on the sibling app_debugger brick's API package)
  • feature_flag_api — FeatureFlagDelegate/FeatureFlagResult contracts and the FeatureFlagSnapshotSource/FlagSnapshot listing contract
  • feature_sdk, identifiers, router_registry, dependencies — shared packages
  • Your UI kit package (uiKitPackageName), for KitSpooferUiKit
  • path_provider, to save overrides in the app support directory
  • app_debugger is generated first when the project lacks it.

Known Limitations ​

  • A provider that does not implement FeatureFlagSnapshotSource adds no flags to the list, and the tool cannot override them. Override such a flag through FeatureFlagSpooferSDK.setOverride.
  • The labels are English. The debugger tools are not localized yet.
  • A bool switch cannot pin a flag to the value the provider already returns: switching to the provider value removes the override.
  • After Fetch now, FeatureFlagManager can return a cached result for up to its cache TTL (5 minutes by default).
  • di/debug_tools.dart is the target on a current project. The marker moved between app_debugger versions, so the hook uses the first file that has it: app_debugger's lib/src/di/debug_tools.dart (2.0.0 and later), its lib/src/app_debugger_sdk_impl.dart (1.3.0 to 1.15.x), then the app's lib/bootstrap.dart (older), and adds the imports and path dependencies to that package. A file that already resolves the SDK is left as it is. If no file has the marker, pre_gen prints the lines to add and stops before it writes any file.

Changelog ​

Run archipelago brick info app_debugger_feature_flag to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.