critical priority high complexity integration pending integration specialist Tier 2

Acceptance Criteria

Map canvas renders full-screen tile layer within MapViewScreen with correct aspect ratio on all target device sizes
Initial camera position is centered on Norway (approximately 65°N, 15°E) at zoom level 5 on first load
Zoom constraints are enforced: minimum zoom 4 (country view), maximum zoom 17 (street level)
Pinch-to-zoom, double-tap zoom, and pan gestures all function correctly and smoothly (≥60fps)
Map canvas is wrapped in Semantics(excludeSemantics: true, label: 'Interactive mentor map. Use the list view for screen reader access.') so VoiceOver/TalkBack skips the tile layer
Two keyboard-accessible zoom buttons (+ / −) are rendered outside the map canvas Semantics boundary with labels 'Zoom in' and 'Zoom out'
Mapbox public access token (tiles only) is stored in app configuration — not hardcoded in source; secret token is never present in mobile app
Map loads tiles progressively — skeleton or loading indicator shown within 200ms of screen mount
If tile loading fails (no network), a non-blocking error banner is shown and the list view toggle is highlighted as the accessible fallback
Map canvas does not leak memory — tile cache is bounded and map controller is disposed on widget unmount
The map provider plugin version is pinned in pubspec.yaml and its public access token is not committed to version control (loaded from --dart-define or environment config)

Technical Requirements

frameworks
Flutter
BLoC
flutter_map or google_maps_flutter
Mapbox Maps SDK for Flutter
apis
Mapbox Maps API (tile rendering via public access token)
performance requirements
Initial tile render within 1.5 seconds on a 4G connection
Map pan and zoom must maintain 60fps on mid-range Android (Snapdragon 665 class)
Tile cache must not exceed 50MB on device
security requirements
Mapbox public access token (for tile rendering only) may be present in app binary — secret token must never be included
No mentor location coordinates are transmitted to Mapbox — markers placed locally after tiles load
Token must be loaded from compile-time --dart-define environment variable, not hardcoded
ui components
FlutterMap or GoogleMap widget (full-screen)
Semantics(excludeSemantics: true) wrapper
Keyboard-accessible zoom control buttons with Semantics labels
Loading skeleton / CircularProgressIndicator
Network error banner widget

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Prefer flutter_map over google_maps_flutter if Mapbox tile hosting is already confirmed — flutter_map is open-source and avoids Google dependency. Use flutter_map's TileLayer with a Mapbox tile URL template. Load the Mapbox public access token from a --dart-define compile-time variable (e.g., MAPBOX_PUBLIC_TOKEN) injected in CI — document this in the project README. The Semantics(excludeSemantics: true) wrapper is critical for Blindeforbundet users: the map canvas is inherently inaccessible to screen readers, so the list view must be the primary accessible path.

Do not attempt to make the map itself fully accessible — instead, ensure the list fallback (task 001) is polished and the toggle button (task 002) is prominently accessible. Use MapController.dispose() in the StatefulWidget's dispose() method to prevent memory leaks. For the tile loading indicator, show it based on a TileLayer onTileError / loading callback rather than a timer.

Testing Requirements

Unit tests: verify map controller is disposed on unmount; verify zoom clamping logic (min 4, max 17). Widget tests: verify Semantics(excludeSemantics: true) is applied to map canvas — use tester.getSemantics() and assert no child semantics nodes under the map; verify zoom buttons are reachable by keyboard traversal; verify loading indicator appears before tiles are ready; verify error banner appears on simulated tile load failure. Integration tests: verify tile loading does not make requests to any endpoint other than the configured Mapbox tile URL. Performance test: 60fps assertion during zoom gesture simulation.

Coverage target: 85%+ on MapViewScreen integration 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.