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

06 — State Management (Shared Architecture)

Recommended client state architecture (Bloc/Cubit). Decision status: proposal — flagged because no client exists yet. Module docs 13_State_Management.md define per-screen states on top of these conventions.


1. Stack

  • flutter_bloc (Bloc + Cubit). Why: strict state transitions, testability, team familiarity.
  • No additional state libs; DI via get_it or providerrecommend get_it + injectable.
  • Server state: repository layer with dio; cache via Hive/shared_preferences for small config and drift only if offline queues grow (YAGNI: start with in-memory + shared_preferences).

2. Layering

UI (widgets) → Cubit/Bloc → Repository → DataSource (dio/WS) → API
                                └─ Cache (memory / prefs)
  • Widgets never call repositories directly; they emit events / read state.
  • Repositories are the only layer touching HTTP; they map envelopes to models and throw typed exceptions (ApiException(code, status, message)).

3. Base patterns (shared)

3.1 Async state machine

sealed class LoadState { Initial, Loading, Success, Error(ApiException) }

Every list/detail cubit exposes LoadState + data. UI maps:

  • Initial/Loading → AppSkeleton
  • Error → AppErrorState(code, message, onRetry)
  • Success + empty → AppEmptyState
  • Success + data → content

3.2 Pagination cubit (mixin)

mixin PaginatedListMixin<T> {
  int page; bool hasNext; bool isLoadingMore; List<T> items;
  // loadFirst() -> emits Loading; loadMore() -> appends; pullToRefresh() -> resets
}

Contract mirrors API: page, limit (default 20, max 100), sort (-field), q. meta from envelope: totalItems, totalPages, hasNext, hasPrevious.

3.3 Cache & staleness

  • Repository caches last successful list per key (sl:cache:{module}:{query} client-side).
  • Stale-while-revalidate: show cache instantly, refresh in background, update on success.
  • TTLs: reference data (classes, subjects) 24 h; volatile lists (attendance today) 5 min; detail views no client cache (server caches).
  • RefreshIndicator always bypasses cache.

3.4 Realtime

  • WS gateway (/ws via WsModule) delivers notification.new, message.new, announcement.published, attendance.changed, invoice.updated events to user rooms.
  • Cubits subscribe via repository subscribe(channel) → pushes events into state.
  • On reconnect: resubscribe + pull-to-refresh equivalent (re-fetch current screen).
  • App in background: WS paused; notifications surface via push; on resume → re-fetch.

3.5 Optimistic updates

  • Mutations that are safe (toggles, mark-read, status changes) apply locally first, then call API; on error: rollback + AppSnackbar(error); on success: reconcile with server payload.
  • Mutations with side effects (payments, publishing results) are never optimistic.

3.6 Auth state

  • AuthCubit: unauthenticated → authenticating → authenticated(user, tenant) → refreshing → sessionExpired.
  • Token refresh on 401 (dio interceptor, single-flight); on refresh failure → sessionExpired → login screen with reason snackbar.
  • Permission changes trigger route rebuild.

3.7 Connectivity state

  • ConnectivityCubit (internet_connection_checker or connectivity_plus): online/offline.
  • Offline: banner (AppOfflineBanner), reads from cache, writes queued (only where module docs define offline write queues — attendance bulk, homework drafts), auto-flush on reconnect with idempotency keys.

4. Event/state conventions

  • Load{Entity}, Refresh{Entity}, LoadMore, Retry, ChangeFilter, Submit, Clear naming.
  • One Cubit per screen; shared selectors for cross-screen data (current user, current academic year, feature flags).
  • FeatureFlagsCubit gates UI per tenant (biometric, SMS, WhatsApp channels).

5. Error handling (client)

Server codeClient behaviour
UNAUTHENTICATED (401)Refresh once; fail → session expiry flow
PERMISSION_DENIED (403)403 screen or hide action; snackbar on inline actions
RESOURCE_NOT_FOUND (404)Empty-state with "not found" copy
VALIDATION_ERROR (400)Field errors mapped to form fields
DUPLICATE_RESOURCE (409)Inline conflict message; suggest search/refresh
BUSINESS_RULE_VIOLATION (422)Message in context (dialog/banner)
RATE_LIMITED (429)"Too many requests — retry in Ns" + backoff
INTERNAL_SERVER_ERROR (5xx)Generic + requestId; retry offered

6. Testing hooks

  • Cubits are pure-Dart, unit-testable with mocked repositories.
  • AppStateObserver logs transitions (dev only).
  • Every cubit has a widget-test pair driving AppSkeleton/AppErrorState/AppEmptyState permutations.