Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15 — Flutter Implementation Guide (Feature Flags Module)

How to build the Feature Flags feature in the Flutter client on top of 00-shared/11. Forward-looking spec; no client repo exists yet. Backend surface is complete (src/modules/feature-flags/**).


1. Folder structure

lib/features/feature_flags/
├── domain/
│   ├── models/
│   │   ├── flag_doc.dart             # key, enabled, label?, description?, module?, version, createdAt, updatedAt
│   │   └── flag_group.dart           # module? + flags (null module = "Ungrouped")
│   └── gating/
│       └── flag_gate.dart            # FlagGate widget + registry (see §6)
├── data/
│   ├── dto/
│   │   └── update_flag_dto.dart      # key, enabled, label?, description?, module?
│   └── repositories/
│       └── feature_flags_repository.dart
└── presentation/
    ├── cubit/
    │   ├── feature_flags_cubit.dart  # GLOBAL gating cubit (app-level, not feature-scoped)
    │   ├── flags_list_cubit.dart
    │   ├── flag_detail_cubit.dart
    │   ├── flag_editor_cubit.dart
    │   └── bulk_update_cubit.dart
    ├── pages/
    │   ├── flags_list_page.dart
    │   ├── flag_detail_page.dart
    │   └── flag_editor_page.dart
    └── widgets/
        ├── flag_row.dart
        ├── flag_key_chip.dart
        ├── flag_group_header.dart
        ├── flag_editor_form.dart
        ├── bulk_update_sheet.dart
        └── affected_map_card.dart

FeatureFlagsCubit is registered in the app-level get_it (single instance) so any module can gate; screen cubits live in this feature folder.

2. Dependencies

flutter_bloc, dio (AppDio), go_router, get_it, intl. No new packages — switches, chips, sheets are Material 3 core.

3. Cubits

  • FeatureFlagsCubit (§13): state {status, enabledKeys, flags, lastUpdated}; API: refresh(), isEnabled(String key), forceRefresh(). Timer-driven TTL 30 s (proposed); subscribe to ConnectivityCubit for refresh-on-reconnect; reset on tenant switch (listen to AuthCubit).
  • FlagsListCubit: filters (module, enabledOnly), groups, per-row pending/error, selection mode for bulk; Toggle(key, value) → optimistic + rollback; Delete(key).
  • FlagDetailCubit: Load/Toggle/Delete/Save.
  • FlagEditorCubit: create vs edit; on submit, keep notSaved flags for description/module (server drops them — feature-flag.repository.ts:40).
  • BulkUpdateCubit: Apply → sequential results; RetryFailed subset.

4. FeatureFlagsRepository (single)

class FeatureFlagsRepository {
  // throws ApiException(code,status)
  Future<List<FlagDoc>> list({String? module});        // GET /feature-flags[?module=]
  Future<List<FlagDoc>> listEnabled();                 // GET /feature-flags/enabled
  Future<FlagDoc> byKey(String key);                   // GET /feature-flags/:key
  Future<FlagDoc> upsert(UpdateFlagDto dto);           // PUT /feature-flags
  Future<List<FlagDoc>> bulkUpsert(List<UpdateFlagDto> dtos); // PUT /feature-flags/bulk
  Future<void> remove(String key);                     // DELETE /feature-flags/:key
}
// org overlay (settings module or here):
Future<Map<String, bool>> orgFlags(String orgId);           // GET /organizations/:id/feature-flags
Future<Map<String, bool>> patchOrgFlags(String orgId, Map<String, bool> merged); // PATCH …

DTO → model: FlagDoc.fromJson maps FeatureFlagDocument fields exactly (feature-flag.schema.ts:9-22 + base.schema.ts timestamps); isDeleted docs never arrive (scoped queries).

5. Navigation

go_router routes (proposed in 04): /settings/feature-flags, :key, :key/edit; guards: authGuard + permissionGuard('feature-flags.read') (target contract — server doesn't enforce yet, rbac.guard.ts:29, OQ-5). Bulk = sheet, not route. Deep links: studylyon://settings/feature-flags?module= and :key.

6. Gating (the module's cross-cutting deliverable)

// shared/gating/flag_gate.dart — used by ANY module
class FlagGate extends StatelessWidget {
  const FlagGate({required this.key, required this.builder, this.fallback});
  final String key; final Widget Function() builder; final Widget? fallback;
  Widget build(context) {
    final flags = context.watch<FeatureFlagsCubit>();
    switch (flags.status) {
      case initial: case loading: return fallback ?? const AppSkeleton.rect();
      default: return flags.isEnabled(key) ? builder() : (fallback ?? const SizedBox.shrink());
    }
  }
}
  • Fail-closed: isEnabled returns false for missing keys (feature-flags.service.ts:24 mirrored); during loading render a skeleton or nothing — never a flash of the feature, never a flash of a "disabled" empty state.
  • Fallback pattern for admin docs: FlagGate(key:'channels.sms', fallback: AppBanner(...)).
  • Registry (proposed, for AppAffectedMapCard): const kFlagScreens = { 'channels.sms': [ScreenRef('SMS compose', '/messages/sms')], … } — static list, reviewed by product.

7. Theme

No new tokens; component mappings in 11_Design_System_Mapping.md all reference existing AppTokens (00-shared/02). Switches follow M3 defaults with primary active.

8. Extensions

  • String.dotToSpaces() / key prettifier for list subtitles (proposed).
  • DateTime.toRelative() for "Updated 2 h ago".
  • bool → Semantics.toggled helper for switch rows.

9. Localization keys

featureflags.list.*, featureflags.detail.*, featureflags.edit.*, featureflags.bulk.*, featureflags.gate.hidden ("Ask your admin to enable this feature"), featureflags.delete.warning (re-create caveat). Server messages mapped to keys for known 404/400 business text; fallback rules per 00-shared/11 §9.

10. Testing

  • Unit: FeatureFlagsCubit state machine + fail-closed isEnabled + TTL timer (fake-async); FlagsListCubit toggle rollback; BulkUpdateCubit partial results; DTO→model mapper (including absent module → "Ungrouped").
  • Widget: list filter/toggle/selection; editor warning banner; bulk results; FlagGate three states (loading → shown / hidden).
  • Golden: components × light/dark × 3 sizes (00-shared/10 §9).
  • Integration: admin toggle → teacher gated screen updates within TTL (mock server); offline gating stays fail-closed.
  • E2E (P0): admin browse → toggle → bulk → delete; teacher gating journey.

11. Performance

  • ListView.builder for groups (flatten groups → index); const constructors; selector (BlocSelector) on enabledKeys so gating rebuilds are scoped to watching widgets.
  • TTL timer pauses in background (app lifecycle) and fires on resume; no timers leaking in tests.
  • List small (tenant catalogs are tens of rows) — no virtualization beyond builder.

12. Proposals flagged to the team

  1. When the server enforces feature-flags.* (OQ-5), wire permissionGuards — UI is already permission-shaped.
  2. When the Redis flag cache + invalidation land (CACHE_ARCHITECTURE.md:33,48, OQ-7), enable the AppFlagToggleBanner "~30 s" copy and drop the client TTL to match.
  3. When description/module persist (OQ-3) and rollout fields exist (OQ-6), extend the editor form (one-line enablement per 08 §1) + percentage/audience controls.
  4. When a WS featureflag.changed topic exists (OQ-7), subscribe in FeatureFlagsCubit and bypass the TTL.
  5. Analytics wiring waits the shared AnalyticsService interface (00-shared/10 §8).