Implement async record count fetching in BLoC
epic-bufdir-report-period-selection-ui-integration-task-003 — Add the FetchRecordCount event handler to PeriodSelectionBloc. On each preset selection or custom range change, debounce and call PeriodRecordCountService with the current DateTimeRange. Emit intermediate counting state and final count state. Cancel in-flight requests when a new range arrives.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Handles integration between different epics or system components. Requires coordination across multiple development streams.
Implementation Notes
Use flutter_bloc's EventTransformer for debouncing: apply `restartable()` or `droppable()` from bloc_concurrency package on the FetchRecordCount handler registration — `on
Compute `showIncompleteWarning` by comparing `selectedRange.end` against a `lastCompleteReportingPeriodEnd` value that should be available from PeriodPresetService or a config constant. Emit state updates using `copyWith` on the existing loaded state to preserve all other fields.
Testing Requirements
Unit tests with bloc_test and fake_async or rxdart test utilities. Required test cases: (1) debounce — dispatch FetchRecordCount 3 times within 100ms, advance fake timer 400ms, verify service called exactly once; (2) cancellation — dispatch FetchRecordCount, advance 200ms, dispatch again, advance 400ms, verify only the second result is emitted; (3) isCountLoading transitions — verify isCountLoading: true emitted before service resolves, isCountLoading: false after; (4) service error — verify recordCount: null and isCountLoading: false emitted, not a PeriodSelectionError full state; (5) showIncompleteWarning — provide a range ending tomorrow, verify warning flag is true in emitted state; (6) SelectPreset trigger — dispatch SelectPreset, verify FetchRecordCount is subsequently processed. Use a MockPeriodRecordCountService that is async-controllable via Completer.
The record count query is asynchronous and may take up to 1 second on a slow connection. If the BLoC does not manage loading states carefully, the UI may show a stale count or the confirm button may be briefly enabled during the loading transition, allowing premature submission.
Mitigation & Contingency
Mitigation: Define explicit BLoC states: PeriodSelectionLoading, PeriodSelectionCountLoaded, PeriodSelectionValidationError, and PeriodSelectionReady. The confirm button must only be enabled in PeriodSelectionReady. Add a debounce of 300ms on period-change events before triggering the count query to prevent excessive calls.
Contingency: If debouncing is insufficient to prevent UX degradation on slow connections, add an optimistic loading skeleton to the RecordCountBanner that clearly communicates a pending state, and keep the confirm button disabled until the count resolves.
The confirmedReportPeriodProvider Riverpod contract between this feature and the Bufdir Export feature may not be defined yet, causing integration failures when the downstream export screen attempts to read the provider.
Mitigation & Contingency
Mitigation: Define the confirmedReportPeriodProvider as a StateProvider<DateTimeRange?> in a shared providers file at the start of this epic, before any screen code is written. Communicate the provider contract to the Bufdir Export feature team so both sides align on the same provider reference.
Contingency: If the export feature consumes the period through a different mechanism (e.g., navigation arguments), add a thin adapter that writes the confirmed period to both the Riverpod provider and the navigation argument, ensuring backward compatibility.
Flutter's live region support for screen readers is inconsistent across platforms (iOS VoiceOver vs Android TalkBack), and the record count banner must be announced on every change — a pattern that has historically required platform-channel workarounds.
Mitigation & Contingency
Mitigation: Test the live region announcement on both iOS (VoiceOver) and Android (TalkBack) early in implementation using the accessibility_test harness. Reference the project's existing live-region-announcer component (664-accessibility-live-region-announcer) for a proven implementation pattern.
Contingency: If native live region support is insufficient, wrap the record count text in a Semantics widget with a programmatically updated label string, and trigger a SemanticsService.announce call on each count update as a platform-agnostic fallback.