13 — State Management (Rooms Module)
- 1. Cubit map
- 2. State machine (generic per 00-shared/06 §3.1)
- 3. RoomListCubit
- 4. RoomDetailCubit
- 5. RoomEditorCubit
- 6. Caching & staleness (module TTLs)
- 7. Realtime & cross-cubit
- 8. Offline & connectivity
- 9. Testing hooks
Cubit architecture per 00-shared/06. One cubit per screen; base
LoadState(Initial/Loading/Success/Error(ApiException)),PaginatedListMixin, cache + SWR, optimistic updates, connectivity. All(proposed)client design.
1. Cubit map
| Cubit | Screen(s) | Data sources | Notes |
|---|---|---|---|
RoomListCubit | S1 | E2 GET /rooms?page&limit (+ client filter (proposed)) | PaginatedListMixin<Room>; local filter/search state; cache invalidation on mutations |
RoomDetailCubit | S2 | E3 GET /rooms/:id | TTL 5 min SWR; reconciles after edit; pops on 404 |
RoomEditorCubit | S3 create/edit | E1 POST / E4 PATCH | form model + validation; 409 inline; dirty tracking |
(future) RoomAvailabilityCubit | S2 availability | bookings module (planned) | (planned) — hidden until bookings lands |
(future) RoomCheckinCubit | QR scan | QR endpoint (forward-looking) | (forward-looking) |
2. State machine (generic per 00-shared/06 §3.1)
stateDiagram-v2
[*] --> Initial
Initial --> Loading: Load
Loading --> Success: load ok
Loading --> Error: ApiException
Error --> Loading: Retry
Success --> Success: Refresh / mutate (reconcile)
Success --> Error: mutation fails (delete/update)
Success --> Loading: pull-to-refresh (bypass cache)
3. RoomListCubit
State: page (1), limit (20), items: List<Room>, hasNext, isLoadingMore, loadState,
filters {types: Set<RoomType>, buildings: Set<String>, query} (proposed) — client-side
until server params land (OQ-3).
Events: LoadFirst(), LoadMore(), Refresh(), SetTypeFilter(t), SetBuildingFilter(b),
SetQuery(q), ClearFilters(), RoomChanged(room) (post-mutation reconcile), RoomDeleted(id).
Load flow: cache hit (sl:{tenant}:rooms:{page}:{limit}, TTL 5 min) → Success(stale:true)
- background refetch; miss → Loading → Success/Error. On
LoadMorewhilehasNext→ append + dedupe by_id.
sequenceDiagram
participant C as RoomListCubit
participant R as RoomsRepository
participant API as GET /rooms?page&limit
C->>C: LoadFirst()
C->>R: list(page:1, limit:20)
API-->>R: data[] + meta {totalItems,hasNext,...}
R-->>C: Success(items, meta)
C->>C: scroll ≥80% && hasNext
C->>R: list(page:2, limit:20)
API-->>R: data[] + meta
R-->>C: append items
4. RoomDetailCubit
State: room, stale (cache-served), loadState.
Events: LoadRoom(id), Refresh(), RoomUpdated(room) (reconcile from editor), Deleted().
Key reducer: Deleted() → emit terminal state; UI pops to list (route-level), list
cubit removes row via RoomDeleted event. 404 → NotFound state (renders "Room not
found" + back), not generic error — because cross-tenant and soft-deleted ids both 404
(rooms.service.ts:38,44,50).
stateDiagram-v2
[*] --> Initial
Initial --> Loading: LoadRoom(id)
Loading --> Success: 200
Loading --> NotFound: 404
Loading --> Error: network/5xx
Success --> Success: RoomUpdated / Refresh
Success --> Deleted: Deleted() → pop to list
NotFound --> Loading: Retry (id re-entered)
5. RoomEditorCubit
State: form: RoomFormModel (F1–F6), initial: RoomFormModel (dirty baseline),
submitting, fieldErrors: Map<String,String>, duplicateHint: bool (proposed),
saveResult.
Events: InitCreate(), InitEdit(room), FieldChanged(field, value), CodeChanged(code)
(triggers debounced duplicate hint against list cache), Submit(), Discard().
Reducers: submit → submitting → on 201/200 emit RoomChanged(room) (via module bus) +
pop + snackbar; on 409 → duplicateHint=true, inline error, no reset; on 400 → fieldErrors
mapped from envelope details[]; on 404 (edit) → pop + "Room not found".
sequenceDiagram
participant W as RoomEditorCubit
participant R as RoomsRepository
participant API as POST /rooms (or PATCH /rooms/:id)
W->>W: Submit()
W->>R: create(dto) / update(id, dto)
alt 201/200
API-->>R: room doc
R-->>W: Success(room) → pop + snackbar
W-->>Bus: RoomChanged(room)
else 409
R-->>W: ApiException(DUPLICATE_RESOURCE)
W-->>UI: inline error under code
else 400
R-->>W: ApiException(VALIDATION_ERROR)
W-->>UI: fieldErrors from details[]
else 404
W-->>UI: pop + "Room not found"
end
6. Caching & staleness (module TTLs)
| Data | Cache key | TTL | Notes |
|---|---|---|---|
| Room list page | sl:{tenant}:rooms:{page}:{limit} | 5 min SWR | invalidated on E1/E4/E5 success |
| Room detail | sl:{tenant}:rooms:{id} | 5 min SWR | reconcile after edit; pop on 404 |
Duplicate-hint code set (proposed) | derived from list cache | — | not authoritative; server 409 is |
Buildings/type dictionary (proposed) | derived from list cache | — | refresh on list refresh |
7. Realtime & cross-cubit
- No WS topics today;
(forward-looking): subscriberooms.updated→ invalidate list + detail caches + refetch (multi-device admin edits). - Cross-cubit invalidation: after E1/E4 success → bump
rooms:{id}+ list keys;RoomDeleted→ list row removal without refetch (delete is void —rooms.service.ts:48). - Module bus events:
RoomChanged(room),RoomDeleted(id)consumed by list; editor listens to nothing (form state is local). - Future
(planned): bookings availability state lives in the bookings module's cubits, not here.
8. Offline & connectivity
- Reads: last-good cache +
AppOfflineBanner;LoadMoreblocked offline (no stale pages). - Writes: blocked in
RoomEditorCubit.Submit()(emit error "You're offline", draft kept). - Reconnect: banner clears; user re-triggers.
9. Testing hooks
- Pure-Dart cubits, mocked repositories; widget tests per state machine
(Loading/Success/Error/NotFound/Empty) + 409-inline + delete-flow (
00-shared/06 §6). - Golden: list with filters applied; editor with duplicate hint; typed-confirm disabled state.