13 — State Management (Settings Module)
- 1. Cubit map
- 2. State objects (concise)
- 3. Cache & staleness
- 4. Optimistic vs full-save (per source semantics)
- 5. Realtime
- 6. Error states per action
- 7. Cross-cutting interplay
- 8. Testing hooks (
00-shared/06 §6)
Per-screen Cubits (Flutter/bloc; proposal, 00-shared/06) backed by
SettingsRepository(dio) which calls the endpoints in 12_API_Mapping.md. Server truth is the upsert response — config state must never diverge from it.
1. Cubit map
| Screen | Cubit | Events → State |
|---|---|---|
| List (hub) | SettingsListCubit | Load(group?), Refresh(), GroupChanged(group), Search(q), EditRow(key), ValueChanged(key, value), SaveRow(key), ToggleBool(key, value), EnterBatch(), SaveAll(), ExitBatch() → {LoadState, settings[] (grouped), selectedGroup, query, dirty: Map<key, SettingDraft>, batchMode, savingKeys: Set} |
| Detail | SettingDetailCubit | Load(key), FieldChanged(field, value), Save(), Delete(), DuplicateKey() → {LoadState, setting, draft, saving, deleted} |
| Create (sheet) | CreateSettingCubit | Create(form) → {idle, saving, created(setting), error(code)} |
Key reducer (list):
GroupChanged(g)→repository.list(group: g)(server filter —settings.controller.ts:26-28), clearsquery+ dirty set.SaveRow(key)→ builds full dto{key, value: draft.value, group: draft.group}→PUT /settings→ on success replace row from server doc (data: Setting,setting.repository.ts:39), remove fromdirty, snackbar; on error keepdirty+ snackbar (retry safe — idempotent).SaveAll()→ serializes only dirty rows (full dto each) →PUT /settings/bulk(settings.service.ts:28-34) → on 200 replace all rows from response array; on 5xx keep dirty set, snackbar "Saved N of M" with per-key retry.Search(q)— client-side filter of the current fetch (no serverqparam).
2. State objects (concise)
class SettingDoc { id, tenantId, key, Object? value, group, isEncrypted,
createdBy?, updatedBy?, version, createdAt, updatedAt; }
class SettingDraft { Object? value; SettingGroup group; } // per-row in dirty map
class SettingsState { LoadState load; List<SettingDoc> rows;
SettingGroup? selectedGroup; String query;
Map<String, SettingDraft> dirty; bool batchMode; Set<String> savingKeys; }
3. Cache & staleness
| Surface | Cache | TTL | Policy |
|---|---|---|---|
List per (group?) | in-memory + shared_preferences last-good | 5 min (config can change via API/other admins) | stale-while-revalidate (00-shared/06 §3.3); RefreshIndicator bypasses |
| Detail | none | — | always fetch on open (GET /settings/:key) |
| Server cache | none in settings path | — | API is single-doc Mongo reads |
Bump the list cache key whenever a save/delete succeeds ({tenant}:settings:{group}) so
sibling devices see fresh values on next focus. On screen focus → Refresh().
4. Optimistic vs full-save (per source semantics)
| Operation | Strategy | Reason (source) |
|---|---|---|
| Boolean toggle | optimistic + rollback | safe mutation class (00-shared/06 §3.5); instant feedback expected |
| String/number/JSON save | server-confirm | value replaced wholesale (setting.repository.ts:37); must reflect server truth |
| Batch save | server-confirm, per-item status | sequential loop, not transactional (settings.service.ts:28-34) |
| Delete | server-confirm; no undo | soft delete irreversible via API (OQ-5) |
| Create | server-confirm | upsert idempotent — retry-safe |
No offline write queue (00-shared/07 §10): offline → banner, writes blocked with
guidance; last-good list still renders.
5. Realtime
- No WS topic for settings (
00-shared/07 §8enumerates no settings channel) → no push-driven invalidation. Refresh-on-focus is the correctness mechanism; a futuresettings.changedtopic is(planned)(OQ-7/8).
6. Error states per action
| Action | Error | State → |
|---|---|---|
| load | 401 | silent refresh → sessionExpired → login |
| load | 5xx | AppErrorState(code, retry) |
| detail load | 404 | AppSettingNotFound (empty state) |
| save | 400 | inline field errors (group enum details) |
| save | 500 | snackbar + form kept (idempotent retry) |
| save (E11000) | 500 | banner "key cannot be re-created" (OQ-5) |
| bulk | 5xx mid-loop | "Saved N of M"; per-key retry re-sends full dirty set |
| delete | 404 | treat as removed |
| any | 429 | countdown, disable submit, no auto-retry |
7. Cross-cutting interplay
ConnectivityCubitgates writes (offline → editors disabled + banner).AuthCubitprovides tenantId/user (displayed in detail meta whencreatedBypresent).FeatureFlagsCubitnot consumed by this module (settings surface is not flag-gated).- Permission changes (role edit) → route rebuild hides
/settingswithoutsettings.read(00-shared/06 §3.6); server not enforcing (OQ-2).
8. Testing hooks (00-shared/06 §6)
- Unit:
SettingsListCubit— group switch triggers server filter call; dirty map lifecycle;SaveAllserializes only dirty full dtos; partial-failure state ("Saved N of M"). - Unit: retry-after-500 keeps dirty set.
- Widget: list loading/error/empty/dirty; JSON editor valid/invalid; batch bar counts.