Connect picker widget to BLoC stream
epic-expense-type-selection-user-interface-task-004 — Subscribe the ExpenseTypePickerWidget to the ExpenseSelectionBLoC stream via StreamBuilder. Map BLoC state (selectedTypes, disabledTypes) onto the widget's props. Dispatch ExpenseTypeSelected events on card tap. Ensure the widget contains zero business logic and passes all BLoC state through directly.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use flutter_bloc's BlocBuilder rather than raw StreamBuilder for consistency with the rest of the codebase. Define buildWhen: (prev, curr) => prev.selectedTypeIds != curr.selectedTypeIds || prev.disabledTypeIds != curr.disabledTypeIds to prevent unnecessary rebuilds on unrelated state changes. The BLoC itself (mutual exclusion logic, API calls) is out of scope for this task — assume it exists and expose its API as a contract. Define the ExpenseSelectionEvent and ExpenseSelectionState sealed classes before starting integration.
Handle all three BLoC state variants (loading, loaded, error) to avoid blank screens. Keep the BlocBuilder in a parent screen widget (e.g., ExpenseTypeSelectionScreen) and pass pure props into ExpenseTypePickerWidget to preserve its testability.
Testing Requirements
Widget + BLoC integration tests: (1) mock ExpenseSelectionBloc with mocktail, emit a loaded state, and verify the picker renders with correct selectedIds and disabledIds, (2) simulate a card tap and verify ExpenseTypeSelected event is added to the bloc, (3) emit a loading state and verify the loading indicator appears, (4) emit an error state and verify the error widget appears with retry button, (5) emit two successive states and verify the widget rebuilds exactly once per emission with buildWhen applied. Use bloc_test helpers (blocTest, whenListen) for clean test setup.
If the expense calculation preview subscribes to the full BLoC state stream, every unrelated state property change (e.g. a loading flag toggle) triggers a widget rebuild. With complex card animations for the disabled-state transition, this could cause frame drops on low-end Android devices used by some peer mentors.
Mitigation & Contingency
Mitigation: Use select() on the Riverpod provider to subscribe only to the specific state slice each widget needs; write a performance test asserting rebuild count on a rapid sequence of toggle events.
Contingency: If jank is detected in device testing, replace animated disabled-state transitions with instant opacity changes and defer animation polish to a follow-up sprint.
The disabled card state requires a specific contrast-safe colour combination that communicates unavailability without relying solely on colour (WCAG 1.4.1). If the current design token palette does not include a disabled-state token with sufficient contrast for text on the disabled background, the widget will either fail WCAG AA or require a last-minute design token addition that could break other components.
Mitigation & Contingency
Mitigation: Audit the existing design token manifest for disabled-state tokens at the start of the epic; if missing, raise with the design lead and add a contrast-validated token before widget implementation begins.
Contingency: If no design review is available, use the established --color-text-disabled and --color-surface-disabled tokens with an added strikethrough or lock icon to satisfy WCAG 1.4.1 non-colour requirement, and document the deviation for design review.