13 — State Management (Communication Module)
- 1. State overview
- 2. AnnouncementFeedCubit
- 3. AnnouncementDetailCubit
- 4. AnnouncementComposerCubit
- 5. AnnouncementReadsCubit
- 6. Cross-cubit communication
- 7. Diagram
- 8. Rules
Cubit-per-screen state, following 00-shared/06. Server truth: announcement documents are immutable after publish (no update route); read state is derived from
readBy[].
1. State overview
| Cubit | Screen | Backend source | Mutation |
|---|---|---|---|
AnnouncementFeedCubit | Feed | GET /announcements | none (list) |
AnnouncementDetailCubit | Detail | carried item + POST :id/read | read receipt |
AnnouncementComposerCubit | Compose | POST /announcements, POST :id/publish | create, publish |
AnnouncementReadsCubit | Receipts | GET /announcements/:id/reads | none (stats) |
ThreadsCubit / ThreadCubit | Threads (secondary) | threads/messages APIs | send, mark read |
All Cubits take the repository/client via constructor injection; single
CommunicationRepository wraps the Dio client (see 15).
2. AnnouncementFeedCubit
States:
FeedInitial
FeedLoading
FeedLoaded(items: List<Announcement>, meId: String, tab: ForMe|All)
FeedError(message)
Events:
FeedFetch() → GET /announcements
FeedRefresh() → refetch
FeedTabChanged(tab) → local refilter (no fetch)
FeedItemRead(id) → optimistic unread-dot removal
- Filtering is client-side:
ForMe=audience.type == 'all' || meId ∈ targetUserIds(announcement.schema.ts:42-43; backend does not filter —announcement.service.ts:56-60). FeedItemReadis optimistic; the authoritativereadByarrives on next fetch — rollback on fetch when server disagrees (idempotent server-side anyway,announcement.repository.ts:20-33).- Caching: last
FeedLoadedkept in memory for offline banner rendering(forward-looking).
3. AnnouncementDetailCubit
States: DetailLoaded(item) | DetailReading | DetailRead | DetailError(message)
Events: DetailMarkRead() → POST /announcements/:id/read
- Single purpose: optimistic checkmark + fire-and-forget receipt POST (silent failure → snackbar; no state rollback needed since re-fetch reconciles).
- Auto-read on open
(proposed)lives here (policy flag from settings).
4. AnnouncementComposerCubit
States:
ComposerIdle(form: {title, body, audience, attachments})
ComposerDirty(form) → on any field change
ComposerSavingDraft → POST /announcements
ComposerDraftSaved(id) → snackbar, stay
ComposerPublishing(id) → POST create, then POST :id/publish
ComposerPublished(id) → pop to feed/mine
ComposerError(message, stage: draft|publish)
- Sequential pipeline: publish = create → publish (
08 §3); a draft-save failure cancels the pipeline; publish failure keeps the created draft id for retry. - Validation is a pure function over the form mirroring
create-announcement.dto.tsrules (08 §5). - No persistence across app restarts
(forward-looking: local draft store).
5. AnnouncementReadsCubit
States: ReadsLoading | ReadsLoaded(readBy, targetCount, tab: read|unread) | ReadsError
Events: ReadsFetch(id) | ReadsSegmentChanged(tab) | ReadsAutoRefresh() (proposed)
- Unread set computed:
targetUserIds − readBy.userId; requirestargetUserIdsfrom the feed item (reads API returns receipts only —announcement.service.ts:104-107). targetCount == 0(ALL audience) ⇒ count UI hidden (09 §3).
6. Cross-cubit communication
FeedItemReadandDetailMarkReaddo not share state directly; the feed reconciles on next fetch. Single source of truth = serverreadBy.- Composer publishes → feed/list must refresh on return (
AnnouncementFeedCubit.FeedRefresh()). - Deep links to detail need an item: resolve from feed cache, else fetch full list and
match id
(forward-looking)— noGET :idAPI (12 §6).
7. Diagram
flowchart LR
subgraph Server
API["GET /announcements"]
READ["POST /announcements/:id/read"]
PUB["POST /announcements<br/>+ :id/publish"]
RDS["GET /announcements/:id/reads"]
end
subgraph Client
FC[AnnouncementFeedCubit]
DC[AnnouncementDetailCubit]
CC[AnnouncementComposerCubit]
RC[AnnouncementReadsCubit]
end
FC -->|fetch/refresh| API
DC -->|mark read| READ
CC -->|create + publish| PUB
RC -->|receipts| RDS
DC -- "read reconciled on refetch" --> FC
CC -- "publish → refresh" --> FC
FC -- "carry item" --> DC
FC -- "carry item (targetUserIds)" --> RC
8. Rules
- Never store
readByas screen truth for other screens — onlyFeedLoadedcache. - Composer keeps its own form state; no redux-style global form.
- All API failures map to typed
ComposerError/FeedErrormessages, never raw exceptions. - Tenant/user ids (
meId) injected once at app bootstrap (auth session) — reused byForMefilter and read checks.