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

15 — Flutter Implementation Guide (Communication Module)

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

PackageUse
flutter_bloc (+ equatable)Cubits per 13_State_Management.md
dio + retrofit (or hand-rolled client)API client, envelope parsing (00-shared/07)
intlrelative dates ("2d ago"), publishedAt formatting
cached_network_imageattachment icons/thumbnails (planned)
connectivity_plusoffline banner (10 §8)
No new state lib, no new DI libreuse 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 FeedLoaded in memory (or shared_preferences cache) 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 showModalBottomSheet with isScrollControlled for receipts.
  • Tablet two-pane via LayoutBuilder / NavigationRail pattern (00-shared/05).
  • Dates via intl with locale from app settings; relative time for feed rows.

11. Build order

  1. Models + repository (+ unit tests, mocked Dio).
  2. Feed Cubit + screen (loading/empty/error/offline states).
  3. Detail sheet + read action.
  4. Composer Cubit + form + audience picker (+ validation mirroring DTO).
  5. Receipts sheet (+ unread derivation).
  6. Threads list/detail (secondary, thin).
  7. WebSocket feed-update subscription (planned)IMPLEMENTATION_PLAN.md:119.

12. Test matrix (unit/widget)

CubitKey tests
FeedForMe filter (all/role/grade/custom), error path, refresh reconcile
Detailoptimistic read, 404 snackbar, idempotent double-tap
ComposerDTO-field validation, publish pipeline failure keeps draft, dirty guard
Readsunread derivation, ALL-audience count hide