critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

AccessibilityStateObserver is a mixin applicable to any BLoC or Cubit class without requiring changes to existing state classes
The mixin exposes an abstract method toAnnouncementString(State state) that maps each state to an optional announcement string
Loading states trigger a polite announcement (e.g., 'Saving activity, please wait')
Success states trigger a polite announcement confirming completion (e.g., 'Activity registered successfully')
Error/validation-failure states trigger an assertive announcement with the error message
Push notification arrivals trigger a polite announcement with the notification title
When a BLoC emits two rapid states (loading → success within 300ms), only the success announcement is dispatched (debounced)
The mixin does not announce state transitions when the screen reader is not active (checked via SemanticsBinding.instance.accessibilityFeatures.accessibleNavigation)
Activity registration BLoC, form validation Cubits, and push notification handler are all wired to the mixin
Removing the mixin from a BLoC compiles cleanly with no residual references
All announcement strings are localised via the app's existing l10n/ARB system

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
apis
SemanticsBinding.instance.accessibilityFeatures
StreamSubscription
BlocObserver
data models
ActivityRegistrationState
FormValidationState
PushNotificationState
performance requirements
Mixin listener must not add more than 0.5ms overhead per state emission
Subscription lifecycle must be tied to BLoC close() to prevent memory leaks
security requirements
Error messages announced by assertive channel must not include stack traces or internal identifiers
Sensitive field values present in state payloads must not be included in announcement strings

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement AccessibilityStateObserver as a Dart mixin on Bloc. In the mixin's initState (or via overriding onCreate in BlocObserver), subscribe to the stream and call toAnnouncementString() for each emission. Use a 300ms debounce (Timer.periodic) to coalesce rapid transitions — only the last emission in the window is announced. Guard all announcements behind a check of SemanticsBinding.instance.accessibilityFeatures.accessibleNavigation to avoid unnecessary TTS calls on non-accessibility devices.

For the global BlocObserver approach, consider extending BlocObserver and registering it in main.dart, mapping known state types to announcement priorities — this avoids modifying every BLoC individually. For push notifications, hook into the existing notification handler stream rather than a BLoC if push events are handled outside BLoC.

Testing Requirements

Write flutter_test widget tests and BLoC unit tests. Test scenarios: (1) loading state emits polite announcement, (2) error state emits assertive announcement, (3) success state emits polite announcement, (4) rapid loading→success coalesces to only success, (5) mixin is silent when accessibleNavigation is false, (6) mixin subscription is cleaned up in BLoC close(). Use MockSemanticsBinding or a spy on LiveRegionAnnouncer provider. Cover all three BLoC integration targets (activity, form, push).

Target ≥85% branch coverage on the mixin.

Component
Live Region Announcer
ui medium
Epic Risks (3)
high impact high prob technical

Flutter's build pipeline and SemanticsService.announce() operate asynchronously. Announcements triggered too early (before the semantic tree settles) may be swallowed silently on both platforms, causing acceptance criteria around the 500ms window to fail intermittently in CI and on device, which would block pilot launch.

Mitigation & Contingency

Mitigation: Implement the LiveRegionAnnouncer with a post-frame callback delay and an internal timing guard. Write integration tests using WidgetTester.pump() sequences that verify announcement delivery across multiple frame boundaries. Validate on physical devices at each sprint boundary, not only in CI.

Contingency: If consistent announcement timing cannot be achieved within Flutter's semantic pipeline, switch to a platform channel approach that calls native UIAccessibility.post (iOS) and AccessibilityManager.announce (Android) directly, bypassing Flutter's intermediary.

high impact medium prob technical

Flutter does not natively emit a focus-gain event to Dart code when VoiceOver or TalkBack moves focus to a specific widget. If the intercept mechanism for the SensitiveFieldWarningDialog relies on an unsupported or undocumented hook in the semantics tree, it may miss focus events for some field types or in some navigation contexts, leaving sensitive data unprotected.

Mitigation & Contingency

Mitigation: Prototype the focus-intercept mechanism during the first sprint of this epic, before building the dialog UI. Evaluate Flutter's SemanticsBinding.instance callbacks and custom SemanticsActions as intercept points. Document the chosen mechanism with platform compatibility notes.

Contingency: If no reliable focus-intercept is available, implement an alternative where sensitive fields show a static 'Screen reader active — tap to reveal' overlay instead of an OS dialog, which is less seamless but achieves equivalent privacy protection without relying on an unreliable event hook.

medium impact low prob dependency

The AccessibilityTestHarness depends on internal flutter_test semantic tree APIs that can change between Flutter minor versions. If the project upgrades Flutter during this epic, the harness may break silently, causing CI accessibility tests to pass while actually skipping assertions.

Mitigation & Contingency

Mitigation: Pin the Flutter SDK version in pubspec.yaml for the duration of this epic. Document which flutter_test APIs are used and their stability tier. Add a canary test that explicitly fails if the semantic tree API surface changes.

Contingency: If a forced Flutter upgrade breaks the harness, prioritise patching the harness as a blocking task before any other epic work continues, using the canary test failure as the trigger.