13 — State Management (Notifications Module)
Flutter client. App-wide conventions in 00-shared/06 (State Management) and 00-shared/11 (Flutter App Architecture); this file is the module slice.
1. Cubits
1.1 NotificationsCubit — inbox list
| State | Fields |
|---|---|
NotificationsInitial | — |
NotificationsLoading | — (first page) |
NotificationsLoaded | items, page, hasNext, totalItems, loadingMore, unreadOnly |
NotificationsEmpty | unreadOnly (drives copy) |
NotificationsError | message, retryable |
Events
LoadFirstPage({ bool unreadOnly })— reset + fetch?page=1&limit=20[&unreadOnly].LoadMore()— guarded byhasNext&& !loadingMore; appends, dedupes by_id(offset pagination can duplicate on insert-on-top, 10 §5).MarkOneRead(id)— optimistic flip, thenPATCH /:id/read; revert on error.MarkAllRead()— optimistic clear, thenPATCH /read-all; revert on error.Refresh()— reload page 1, keep scroll position.
Dependencies: NotificationsApi (see 15 §4), UnreadCountCubit (count
reconciliation after mark mutations).
1.2 UnreadCountCubit — shell badge
| State | Fields |
|---|---|
UnreadCountState | count (int ≥ 0), loaded (bool — false → hide badge silently, 06 §3) |
Events: Refresh() (GET /unread-count), Decrement() (optimistic, from
NotificationsCubit), Reset() (after mark-all), Increment() (forward-looking:
realtime via WebSocket /ws — END_TO_END_USER_FLOWS.md:773-775).
Refresh triggers: app foreground, inbox open, after any mark mutation (server value wins over optimistic).
2. State Flow
flowchart TD
A[AppShell] -->|badge| UC[UnreadCountCubit]
A -->|"GET /unread-count"| UC
B[InboxScreen] --> NC[NotificationsCubit]
NC -->|"GET /notifications?page&limit&unreadOnly"| API
subgraph Mutations
NC -->|"PATCH /:id/read"| API
NC -->|"PATCH /read-all"| API
end
API -->|"envelope {data, meta}"| NC
NC -->|"onMutation success → Refresh"| UC
UC -->|count| B[Badge Widget]
NC -->|items| T[NotificationListTile]
3. Consistency Rules
- Server is truth after every mutation: optimistic UI → request → reconcile
with response (doc
readAt, or freshunread-count). - Badge = server count; optimistic decrements are cosmetic and re-synced on foreground/inbox-open.
unreadOnlyfilter change resets the list (newLoadFirstPage).- Pagination state lives in the Cubit, never in widget locals
(
00-shared/06— single source per feature). - Offline hydration: cache last
Loadedvia app-wide cache layer (00-shared/06 §hydration); refresh on reconnect.
4. What NOT to store in state
- Relative-time strings (recomputed on rebuild).
- Icon mappings (pure functions in one file —
11 §3). - Auth token / tenant (shell concerns,
00-shared/06).