15 — Flutter Implementation Guide (Communication Module)
- 1. Packages
- 2. Folder layout (module slice under
features/) - 3. Model (mirror the schema exactly)
- 4. Repository (API surface, from
12_API_Mapping) - 5. Feed Cubit (core shape)
- 6. Composer flow (sequential publish)
- 7. Read action (idempotent, silent-fail)
- 8. Audience picker (names not ids!)
- 9. Offline & resilience
(forward-looking) - 10. A11y & platform
- 11. Build order
- 12. Test matrix (unit/widget)
Build order, packages, folder layout, key code shapes. Client is forward-looking (not in PRD — 01 §2). Follows 00-shared/11 app architecture and 00-shared/06 state conventions.
1. Packages
| Package | Use |
|---|---|
flutter_bloc (+ equatable) | Cubits per 13_State_Management.md |
dio + retrofit (or hand-rolled client) | API client, envelope parsing (00-shared/07) |
intl | relative dates ("2d ago"), publishedAt formatting |
cached_network_image | attachment icons/thumbnails (planned) |
connectivity_plus | offline banner (10 §8) |
| No new state lib, no new DI lib | reuse app-wide choices (00-shared/11) |
2. Folder layout (module slice under features/)
lib/features/communication/
├── data/
│ ├── models/announcement.dart # mirrors announcement.schema.ts:31-53
│ ├── models/announcement_audience.dart
│ ├── models/read_receipt.dart # {userId, readAt}
│ └── communication_repository.dart # Dio calls → typed results
├── domain/ # (thin — skip unless app requires it)
├── application/
│ ├── announcement_feed_cubit.dart
│ ├── announcement_detail_cubit.dart
│ ├── announcement_composer_cubit.dart
│ └── announcement_reads_cubit.dart
└── presentation/
├── screens/announcement_feed_screen.dart
├── screens/announcement_detail_sheet.dart
├── screens/announcement_composer_screen.dart
├── screens/announcement_reads_sheet.dart
└── widgets/ # components from 07
├── announcement_card.dart
├── audience_chip.dart
├── announcement_status_badge.dart
├── read_receipt_bar.dart
├── read_receipt_row.dart
└── audience_picker.dart
3. Model (mirror the schema exactly)
enum AudienceType { all, role, grade, section, custom } // announcement.schema.ts:7-13
class AnnouncementAudience {
final AudienceType type;
final String? value; // string | string[] — model as StringListAudience if needed
}
class AnnouncementReadReceipt { final String userId; final DateTime readAt; }
class Announcement {
final String id, title, body;
final AnnouncementAudience audience;
final List<String> targetUserIds; // schema :42-43
final bool published; // :45-46
final DateTime? publishedAt; // :48-49
final List<AnnouncementReadReceipt> readBy; // :51-52
final List<String> attachments;
final String createdBy, createdAt, updatedAt;
}
Field names snake_case → camelCase per 00-shared/07 mapping convention.
4. Repository (API surface, from 12_API_Mapping)
class CommunicationRepository {
Future<List<Announcement>> listAnnouncements({AudienceType? audience}); // GET /announcements
Future<Announcement> createAnnouncement(CreateAnnouncementDto dto); // POST /announcements
Future<void> publishAnnouncement(String id); // POST :id/publish
Future<void> markAnnouncementRead(String id); // POST :id/read
Future<List<AnnouncementReadReceipt>> announcementReads(String id); // GET :id/reads
}
CreateAnnouncementDto mirrors create-announcement.dto.ts:29-49: title, body,
audience{type, value?}, attachments[]? — nothing else.
5. Feed Cubit (core shape)
class AnnouncementFeedCubit extends Cubit<AnnouncementFeedState> {
AnnouncementFeedCubit(this._repo, this._meId) : super(FeedInitial());
Future<void> fetch() async {
emit(FeedLoading());
try {
final items = await _repo.listAnnouncements();
emit(FeedLoaded(items: items, meId: _meId, tab: _currentTab));
} catch (e) {
emit(FeedError(message: friendly(e))); // typed errors, never raw
}
}
bool isForMe(Announcement a) =>
a.audience.type == AudienceType.all || a.targetUserIds.contains(_meId);
}
6. Composer flow (sequential publish)
Future<void> publish() async {
if (!valid(_form)) return emit(ComposerError(validation: _form.errors));
emit(ComposerSavingDraft());
try {
final draft = await _repo.createAnnouncement(dto(_form)); // POST → draft
emit(ComposerPublishing(id: draft.id));
await _repo.publishAnnouncement(draft.id); // POST :id/publish
emit(ComposerPublished(id: draft.id)); // pop + refresh feed
} on DioException catch (e) {
emit(ComposerError(stage: e.requestOptions.path.contains('/publish') ? 'publish' : 'draft',
message: friendly(e)));
}
}
7. Read action (idempotent, silent-fail)
void markRead(String id) {
emit(DetailRead()); // optimistic checkmark
unawaited(_repo.markAnnouncementRead(id) // POST :id/read — $addToSet safe
.catchError((_) => null)); // snackbar on error only; refetch reconciles
}
8. Audience picker (names not ids!)
Grade/section targeting uses names (announcement.service.ts:121,128). Widget must
offer options sourced from the academics module list (name strings), not free text;
custom uses a user-search chip flow. Show recipient preview (planned).
9. Offline & resilience (forward-looking)
- Feed: persist last
FeedLoadedin memory (orshared_preferencescache) for offline banner rendering. - Read receipts: local queue flushed on reconnect — safe because server dedupes
(
$addToSet,announcement.repository.ts:27-31). - No offline compose persistence in v1 (keep in-memory form only).
10. A11y & platform
- Unread dot:
Semantics(label: 'Unread', child: dot)— never color-only (11 §3). - Sheets use
showModalBottomSheetwithisScrollControlledfor receipts. - Tablet two-pane via
LayoutBuilder/NavigationRailpattern (00-shared/05). - Dates via
intlwith locale from app settings; relative time for feed rows.
11. Build order
- Models + repository (+ unit tests, mocked Dio).
- Feed Cubit + screen (loading/empty/error/offline states).
- Detail sheet + read action.
- Composer Cubit + form + audience picker (+ validation mirroring DTO).
- Receipts sheet (+ unread derivation).
- Threads list/detail (secondary, thin).
- WebSocket feed-update subscription
(planned)—IMPLEMENTATION_PLAN.md:119.
12. Test matrix (unit/widget)
| Cubit | Key tests |
|---|---|
| Feed | ForMe filter (all/role/grade/custom), error path, refresh reconcile |
| Detail | optimistic read, 404 snackbar, idempotent double-tap |
| Composer | DTO-field validation, publish pipeline failure keeps draft, dirty guard |
| Reads | unread derivation, ALL-audience count hide |