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

13 — State Management (Communication Module)

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

CubitScreenBackend sourceMutation
AnnouncementFeedCubitFeedGET /announcementsnone (list)
AnnouncementDetailCubitDetailcarried item + POST :id/readread receipt
AnnouncementComposerCubitComposePOST /announcements, POST :id/publishcreate, publish
AnnouncementReadsCubitReceiptsGET /announcements/:id/readsnone (stats)
ThreadsCubit / ThreadCubitThreads (secondary)threads/messages APIssend, 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).
  • FeedItemRead is optimistic; the authoritative readBy arrives on next fetch — rollback on fetch when server disagrees (idempotent server-side anyway, announcement.repository.ts:20-33).
  • Caching: last FeedLoaded kept 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.ts rules (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; requires targetUserIds from 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

  • FeedItemRead and DetailMarkRead do not share state directly; the feed reconciles on next fetch. Single source of truth = server readBy.
  • 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) — no GET :id API (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

  1. Never store readBy as screen truth for other screens — only FeedLoaded cache.
  2. Composer keeps its own form state; no redux-style global form.
  3. All API failures map to typed ComposerError/FeedError messages, never raw exceptions.
  4. Tenant/user ids (meId) injected once at app bootstrap (auth session) — reused by ForMe filter and read checks.