06 — State Management (Shared Architecture)
- 1. Stack
- 2. Layering
- 3. Base patterns (shared)
- 4. Event/state conventions
- 5. Error handling (client)
- 6. Testing hooks
Recommended client state architecture (Bloc/Cubit). Decision status: proposal — flagged because no client exists yet. Module docs
13_State_Management.mddefine 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_itorprovider— recommendget_it+ injectable. - Server state: repository layer with
dio; cache viaHive/shared_preferencesfor small config anddriftonly 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).
RefreshIndicatoralways bypasses cache.
3.4 Realtime
- WS gateway (
/wsviaWsModule) deliversnotification.new,message.new,announcement.published,attendance.changed,invoice.updatedevents 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 (
diointerceptor, 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,Clearnaming.- One Cubit per screen; shared selectors for cross-screen data (current user, current academic year, feature flags).
FeatureFlagsCubitgates UI per tenant (biometric, SMS, WhatsApp channels).
5. Error handling (client)
| Server code | Client 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.
AppStateObserverlogs transitions (dev only).- Every cubit has a widget-test pair driving
AppSkeleton/AppErrorState/AppEmptyStatepermutations.