Implement MentorMapBloc with dual state machines
epic-geographic-peer-mentor-map-core-services-task-006 — Implement MentorMapBloc using flutter_bloc with two parallel state machines: one for map view state (loading, loaded, error, empty) and one for filter panel state (inactive, active, applying). Events: BoundingBoxChanged, FiltersApplied, FiltersReset, RefreshRequested. The BLoC calls MentorLocationService and emits state updates consumed by the map view and list fallback. Include debounce on BoundingBoxChanged events.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Model MentorMapState as a single immutable class with two fields: mapView (MapViewState) and filterPanel (FilterPanelState), both sealed classes. Use copyWith to produce the next state, which keeps event handlers concise. For debounce, use EventTransformer from the bloc_concurrency package (restartable transformer) on the BoundingBoxChanged event handler — this also cancels in-flight fetches automatically. Do not use two separate BLoCs; keeping both state machines in one BLoC avoids cross-BLoC synchronisation complexity and ensures atomic state transitions.
Use equatable on all state and event classes. Keep MentorLocationService injection via constructor for testability. The BLoC should not know about the map widget library (e.g., flutter_map or google_maps) — all geographic types should be project-defined (BoundingBox, LatLng) so the BLoC remains UI-framework agnostic.
Testing Requirements
Use bloc_test package for all BLoC tests. Write an expect sequence test for each event type covering: the initial state, intermediate states (e.g., MapViewLoading emitted before MapViewLoaded), and the final steady state. Test debounce by emitting multiple rapid BoundingBoxChanged events and asserting that MentorLocationService.fetchMentorsInBoundingBox is called only once. Test concurrent event handling: emit FiltersApplied during an in-flight BoundingBoxChanged fetch and verify the BLoC does not emit inconsistent states.
Test error propagation: mock MentorLocationService to throw a NetworkException and verify MapViewError is emitted. Test cache hit: mock service to return CacheHitState and verify lastUpdated is present in MapViewLoaded. Minimum 95% branch coverage on event handlers.
The dual BLoC state machines (map view state + filter state) may introduce subtle synchronisation bugs where filter changes do not correctly re-trigger viewport queries, causing stale data to appear on the map.
Mitigation & Contingency
Mitigation: Define all BLoC state transitions in a state diagram before implementation. Use flutter_bloc's BlocObserver in development mode to log every state transition. Write explicit unit tests for filter-change → re-query transitions.
Contingency: If state synchronisation bugs appear in integration testing, refactor to a single unified BLoC that owns both map viewport state and filter state, eliminating cross-BLoC dependencies.
Cached mentor location data may become stale (mentors move, pause, or revoke consent) and coordinators in offline mode could be shown incorrect mentor information, leading to wasted outreach.
Mitigation & Contingency
Mitigation: Display a clear timestamp on cached data indicating when it was last synced. Set cache TTL to 24 hours and show an 'offline — data from [date]' banner. Revoked consent removes the mentor from the cache on next successful sync via contact-cache-sync-repository.
Contingency: If cache staleness causes user complaints, reduce TTL to 4 hours and implement background sync on app foreground. Accept that very-recently-revoked mentors may appear briefly in offline mode — document this as a known limitation in the privacy policy.