critical priority high complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

All consented mentors from the BLoC's loaded state are rendered as MapMarkerWidget instances on the map canvas
Mentors with isConsented=false are never rendered as map markers under any circumstances
Marker clustering activates when ≥3 markers are within a 40dp radius on screen; cluster shows a badge with the mentor count
Cluster badge count is accurate — no double-counting when zooming in and out rapidly
Applying availability status or specialisation filters hides non-matching markers client-side with no Supabase query issued
Filtering updates the visible marker set within 100ms for up to 500 mentors
A live region Semantics announcement is emitted after each filter application: '{N} mentors visible' (e.g., '23 mentors visible')
Tapping a cluster zooms the camera to fit all markers within the cluster bounds
Tapping an individual marker (not a cluster) emits MarkerSelected(mentorId) and does not cause map camera movement
Marker layer re-renders only when mentor list or filter state changes — not on unrelated BLoC state emissions
Memory usage does not grow unboundedly — markers removed from BLoC state are correctly removed from the map layer

Technical Requirements

frameworks
Flutter
BLoC
flutter_map or google_maps_flutter
data models
assignment
contact
performance requirements
Client-side filter application must complete in under 100ms for 500 mentor records
Marker layer rebuild must complete within one frame (16ms) when filter state changes
Clustering algorithm must complete in under 50ms for 500 markers on the main thread; use isolate if needed
security requirements
Only consented mentor locations rendered — enforced in the data layer before BLoC emits state
Location precision must be municipality-level only — exact GPS coordinates must not be used for marker placement
No PII logged or emitted during marker rendering
ui components
MarkerLayer (flutter_map) or MarkerClusterer (google_maps_flutter)
MapMarkerWidget (from task-003)
Cluster badge widget with count label
Semantics live region for marker count announcements

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use BlocSelector to derive the visible marker list from two BLoC fields: List and MapFilterState. This ensures the marker layer only rebuilds when either of these fields changes, not on popup open/close or other UI state changes. Implement client-side filtering as a pure function filterMentors(List all, MapFilterState filter) → List — this makes it easily unit-testable. For clustering, use the flutter_map_marker_cluster package (if using flutter_map) or the built-in clustering in google_maps_flutter.

The live region announcement must use a Semantics widget with liveRegion: true placed outside the map canvas excludeSemantics boundary — typically in a transparent overlay at the top of the screen. Post the announcement string to a StreamController that drives the live region widget. Use a debounce of 500ms on live region updates to avoid rapid successive announcements during filter dragging.

Testing Requirements

Unit tests: verify filter application logic produces correct subset of markers for each filter combination; verify clustering groups markers within 40dp correctly; verify live region text format is '{N} mentors visible'. Widget tests: verify BlocSelector only triggers rebuild on mentor list or filter state changes (not on other state fields); verify cluster tap triggers camera fit animation; verify individual marker tap emits correct MarkerSelected event. Integration tests: load 100 mock mentors, apply filter, assert correct visible count and live region announcement. Performance test: assert filter application completes under 100ms with 500 mentor records using Stopwatch in widget test.

Coverage target: 90%+ on rendering and filtering logic.

Component
Map View Screen
ui high
Epic Risks (3)
high impact high prob technical

Flutter's map canvas (flutter_map) does not natively support semantic focus traversal for screen readers, meaning map markers may be entirely invisible to VoiceOver/TalkBack users. If the accessible list fallback is not treated as a first-class view, screen reader users will have no access to the feature.

Mitigation & Contingency

Mitigation: From sprint 1, treat mentor-list-fallback as a fully featured primary view, not an afterthought. Implement and test it in parallel with the map canvas. Make the view-toggle-button keyboard focusable and announced on every screen state. Conduct VoiceOver testing on device before submitting each PR touching UI components.

Contingency: If map canvas accessibility cannot be achieved for marker focus traversal, make the view-toggle-button the default focus target on screen load for VoiceOver users (detected via screen-reader-detection-service) so they are immediately directed to the list fallback without needing to discover the toggle.

medium impact medium prob technical

The mentor-info-popup must occupy no more than 40% of visible map area on small screens. On devices with screen heights under 667px (iPhone SE), overlapping with the filter panel or obscuring most of the map could severely degrade usability.

Mitigation & Contingency

Mitigation: Implement the popup as a bottom sheet capped at 40% of screen height with a ScrollView for overflow content. Test on iPhone SE (375x667pt) and the smallest commonly used Android form factor in the device lab. Define max-height as a percentage constant in location-privacy-config or design tokens.

Contingency: If the popup cannot fit all required fields within 40% height on smallest targets, truncate assigned contact count and certification badge to icons-only in the compact view, with a 'View Profile' button always visible at the bottom of the popup regardless of scroll position.

medium impact medium prob integration

Filter state must remain perfectly synchronised between the map view and the list fallback. If the filter panel emits state that is not consumed identically by both views, coordinators switching between views will see inconsistent mentor sets, eroding trust in the feature.

Mitigation & Contingency

Mitigation: Store active filter criteria in a single shared Riverpod provider owned by the map-view-screen and consumed by both map-marker-widget (via mentor-location-service) and mentor-list-fallback. Write integration tests that apply a filter, switch views, and assert identical mentor counts in both views.

Contingency: If filter sync proves brittle, simplify to a single filter state object passed explicitly as a constructor argument to both views on each rebuild, eliminating indirect state sharing.